/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "interface/renderer.h"

#include "common/util.h"
#include "common/array.h"
#include "common/debug.h"
#include "common/option.h"
#include "common/sysinfo.h"
#include "common/rects.h"
#include "common/time.h"
#include "common/locking.h"
#include "app.h"
#include "util.h"

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl32.h>

#include "cimgui.h"
#include "cimgui_impl.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include "egl_dynprocs.h"
#include "model.h"
#include "shader.h"
#include "state.h"
#include "damage.h"
#include "desktop.h"
#include "cursor.h"
#include "hdr_overlay.h"
#include "hdr_compose.h"
#include "postprocess.h"
#include "util.h"

#define MAX_BUFFER_AGE        3
#define DESKTOP_DAMAGE_COUNT  4
#define DESKTOP_DAMAGE_MARGIN 1
#define MAX_ACCUMULATED_DAMAGE ((LG_MAX_FRAME_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2) * MAX_BUFFER_AGE)
#define IDX_AGO(counter, i, total) (((counter) + (total) - (i)) % (total))

struct Options
{
  bool vsync;
  bool doubleBuffer;
};

struct Inst
{
  LG_Renderer base;

  bool dmaSupport;
  LG_RendererParams params;
  struct Options    opt;

  EGLNativeWindowType  nativeWind;
  EGLDisplay           display;
  EGLConfig            configs;
  EGLSurface           surface;
  EGLContext           context, frameContext;

  EGL_Desktop     * desktop; // the desktop
  EGL_Cursor      * cursor;  // the mouse cursor
  EGL_Damage      * damage;  // the damage display
  EGL_HDROverlay  * hdrOverlay;
  EGL_HDRCompose  * hdrCompose;
  bool              imgui;   // if imgui was initialized

  LG_RendererFormat    format;
  bool                 formatValid;

  int               width, height;
  float             uiScale;
  struct DoubleRect destRect;
  LG_RendererRotate rotate; //client side rotation

  float translateX  , translateY;
  float scaleX      , scaleY;
  float splashRatio;
  float screenScaleX, screenScaleY;

  int viewportWidth, viewportHeight;
  enum EGL_DesktopScaleType scaleType;

  bool  cursorVisible;
  int   cursorX    , cursorY;
  int   cursorHX   , cursorHY;
  float mouseWidth , mouseHeight;
  float mouseScaleX, mouseScaleY;
  bool  showDamage;
  bool  scalePointer;

  struct CursorState cursorLast;

  bool                 hadOverlay;
  bool                 noSwapDamage;
  struct DesktopDamage desktopDamage[DESKTOP_DAMAGE_COUNT];
  unsigned int         desktopDamageIdx;
  LG_Lock              desktopDamageLock;

  bool         hasBufferAge;
  struct Rect  overlayHistory[DESKTOP_DAMAGE_COUNT][MAX_OVERLAY_RECTS + 1];
  int          overlayHistoryCount[DESKTOP_DAMAGE_COUNT];
  unsigned int overlayHistoryIdx;

  bool showSwSurface;
  int  swSurfaceWidth, swSurfaceHeight;

  bool surfaceSupportsPQ;
  bool surfaceSupportsSCRGB;
  LG_RendererCaptureFormat captureFormat;
  bool hdr;               // true if current frame format is HDR
  bool nativeHDR;         // true only while the surface HDR description is active
  bool nativeHDRPQ;
  float nativeReferenceWhiteLevel;
  bool linearHDRComposition;
};

static struct Option egl_options[] =
{
  {
    .module       = "egl",
    .name         = "vsync",
    .description  = "Enable vsync",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = false,
  },
  {
    .module       = "egl",
    .name         = "doubleBuffer",
    .description  = "Enable double buffering",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = false
  },
  {
    .module       = "egl",
    .name         = "multisample",
    .description  = "Enable Multisampling",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = true
  },
  {
    .module       = "egl",
    .name         = "nvGainMax",
    .description  = "The maximum night vision gain",
    .type         = OPTION_TYPE_INT,
    .value.x_int  = 1
  },
  {
    .module       = "egl",
    .name         = "nvGain",
    .description  = "The initial night vision gain at startup",
    .type         = OPTION_TYPE_INT,
    .value.x_int  = 0
  },
  {
    .module       = "egl",
    .name         = "cbMode",
    .description  = "Color Blind Mode (0 = Off, 1 = Protanope, 2 = Deuteranope, 3 = Tritanope)",
    .type         = OPTION_TYPE_INT,
    .value.x_int  = 0
  },
  {
    .module       = "egl",
    .name         = "scale",
    .description  = "Set the scale algorithm (0 = auto, 1 = nearest, 2 = linear)",
    .type         = OPTION_TYPE_INT,
    .validator    = egl_desktopScaleValidate,
    .value.x_int  = 0
  },
  {
    .module       = "egl",
    .name         = "debug",
    .description  = "Enable debug output",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = false
  },
  {
    .module       = "egl",
    .name         = "noBufferAge",
    .description  = "Disable partial rendering based on buffer age",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = false
  },
  {
    .module       = "egl",
    .name         = "noSwapDamage",
    .description  = "Disable swapping with damage",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = false
  },
  {
    .module       = "egl",
    .name         = "scalePointer",
    .description  = "Keep the pointer size 1:1 when downscaling",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = true
  },
  {
    .module       = "egl",
    .name         = "mapHDRtoSDR",
    .description  = "Map HDR content to the SDR color space",
    .type         = OPTION_TYPE_BOOL,
    .value.x_bool = true
  },
  {
    .module       = "egl",
    .name         = "peakLuminance",
    .description  = "The peak luminance level in nits for HDR to SDR mapping",
    .type         = OPTION_TYPE_INT,
    .value.x_int  = 250,
  },
  {
    .module       = "egl",
    .name         = "maxCLL",
    .description  = "Maximum content light level in nits for HDR to SDR mapping",
    .type         = OPTION_TYPE_INT,
    .value.x_int  = 10000,
  },

  {0}
};

static const char * egl_getName(void)
{
  return "EGL";
}

static void egl_setup(void)
{
  option_register(egl_options);
  egl_postProcessEarlyInit();
}

static bool egl_create(LG_Renderer ** renderer, const LG_RendererParams params,
    bool * needsOpenGL)
{
  // check if EGL is even available
  if (!eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS))
  {
    DEBUG_WARN("EGL is not available");
    return false;
  }

  // create our local storage
  struct Inst * this = calloc(1, sizeof(*this));
  if (!this)
  {
    DEBUG_INFO("Failed to allocate %lu bytes", sizeof(*this));
    return false;
  }
  *renderer = &this->base;

  // safe off parameteres and init our default option values
  memcpy(&this->params, &params, sizeof(LG_RendererParams));

  this->opt.vsync        = option_get_bool("egl", "vsync");
  this->opt.doubleBuffer = option_get_bool("egl", "doubleBuffer");

  this->translateX   = 0;
  this->translateY   = 0;
  this->scaleX       = 1.0f;
  this->scaleY       = 1.0f;
  this->screenScaleX = 1.0f;
  this->screenScaleY = 1.0f;
  this->uiScale      = 1.0;

  LG_LOCK_INIT(this->desktopDamageLock);
  this->desktopDamage[0].count = -1;

  *needsOpenGL = false;
  return true;
}

static bool egl_initialize(LG_Renderer * renderer)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  DEBUG_INFO("Double buffering is %s", this->opt.doubleBuffer ? "on" : "off");
  return true;
}

static void egl_deinitialize(LG_Renderer * renderer)
{
  struct Inst * this = UPCAST(struct Inst, renderer);

  if (this->imgui)
  {
    ImGui_ImplOpenGL3_Shutdown();
    egl_stateInvalidate();
  }

  egl_desktopFree(&this->desktop);
  egl_cursorFree (&this->cursor);
  egl_damageFree (&this->damage);
  egl_hdrOverlayFree(&this->hdrOverlay);
  egl_hdrComposeFree(&this->hdrCompose);

  LG_LOCK_FREE(this->lock);
  LG_LOCK_FREE(this->desktopDamageLock);

  eglMakeCurrent(this->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

  if (this->frameContext)
    eglDestroyContext(this->display, this->frameContext);

  if (this->context)
    eglDestroyContext(this->display, this->context);

  eglTerminate(this->display);

  free(this);
}

static bool egl_supports(LG_Renderer * renderer, LG_RendererSupport flag)
{
  struct Inst * this = UPCAST(struct Inst, renderer);

  switch(flag)
  {
    case LG_SUPPORTS_DMABUF:
      return this->dmaSupport;

    case LG_SUPPORTS_HDR_PQ:
      return this->surfaceSupportsPQ &&
        egl_hdrComposeIsConfigured(this->hdrCompose);

    case LG_SUPPORTS_HDR_SCRGB:
      return this->surfaceSupportsSCRGB;

    default:
      return false;
  }
}

static bool egl_getInterop(LG_Renderer * renderer,
    LG_RendererInterop * interop)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  if (!this->display || !this->context || !this->configs ||
      interop->size < sizeof(*interop))
    return false;

  interop->version          = LG_RENDERER_INTEROP_VERSION;
  interop->type             = LG_RENDERER_INTEROP_EGL;
  interop->egl.display      = this->display;
  interop->egl.config       = this->configs;
  interop->egl.shareContext = this->context;
  interop->egl.dmaBufImport = this->dmaSupport;
  return true;
}

static void egl_onRestart(LG_Renderer * renderer)
{
  struct Inst * this = UPCAST(struct Inst, renderer);

  if (this->frameContext)
  {
    egl_stateCheckShared();
    egl_desktopRestart(this->desktop);
    eglDestroyContext(this->display, this->frameContext);
    this->frameContext = NULL;
  }

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    this->desktopDamage[this->desktopDamageIdx].frameToken =
      LG_RENDERER_FRAME_TOKEN_NONE;
    this->desktopDamage[this->desktopDamageIdx].count = -1;
  });
}

static void egl_getDesktopSize(
    const struct Inst * this, int * width, int * height)
{
  if (this->showSwSurface)
  {
    *width  = this->swSurfaceWidth;
    *height = this->swSurfaceHeight;
    return;
  }

  *width  = this->formatValid ? this->format.frameWidth  : 0;
  *height = this->formatValid ? this->format.frameHeight : 0;
}

static void egl_invalidateDesktopDamage(struct Inst * this)
{
  INTERLOCKED_SECTION(this->desktopDamageLock, {
    struct DesktopDamage * damage =
      this->desktopDamage + this->desktopDamageIdx;
    damage->frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
    damage->count      = -1;
  });
}

static void egl_update_pointer_scale(struct Inst * this)
{
  if (!this->scalePointer || this->width <= 0 || this->height <= 0)
    return;

  int width, height;
  if (this->showSwSurface)
  {
    width  = this->swSurfaceWidth;
    height = this->swSurfaceHeight;
  }
  else if (this->formatValid)
  {
    width  = this->format.screenWidth;
    height = this->format.screenHeight;
  }
  else
    return;

  egl_cursorSetScale(this->cursor, max(1.0f, max(
      (float)width  / this->width,
      (float)height / this->height)));
}

static void egl_addSwSurfaceDamage(
    struct Inst * this, int x, int y, int width, int height)
{
  if (width <= 0 || height <= 0 ||
      this->swSurfaceWidth <= 0 || this->swSurfaceHeight <= 0)
    return;

  const int64_t right  = min(
      (int64_t)x + width, (int64_t)this->swSurfaceWidth);
  const int64_t bottom = min(
      (int64_t)y + height, (int64_t)this->swSurfaceHeight);
  x = max(x, 0);
  y = max(y, 0);
  if (right <= x || bottom <= y)
    return;

  const FrameDamageRect rect = {
    .x      = (uint32_t)x,
    .y      = (uint32_t)y,
    .width  = (uint32_t)(right  - x),
    .height = (uint32_t)(bottom - y),
  };

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    if (this->showSwSurface)
    {
      struct DesktopDamage * damage =
        this->desktopDamage + this->desktopDamageIdx;
      damage->frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
      if (damage->count >= 0)
      {
        if (damage->count + 1 >= LG_MAX_FRAME_DAMAGE_RECTS)
          damage->count = -1;
        else
          damage->rects[damage->count++] = rect;
      }
    }
  });
}

static void egl_calc_mouse_size(struct Inst * this)
{
  if (this->showSwSurface)
  {
    this->mouseScaleX = 2.0f / this->swSurfaceWidth;
    this->mouseScaleY = 2.0f / this->swSurfaceHeight;
    egl_cursorSetSize(this->cursor,
      (this->mouseWidth  *
        (1.0f / this->swSurfaceWidth )) * this->scaleX,
      (this->mouseHeight *
        (1.0f / this->swSurfaceHeight)) * this->scaleY
    );
    return;
  }

  if (!this->formatValid)
    return;

  int w, h;

  switch(this->format.rotate)
  {
    case LG_ROTATE_0:
    case LG_ROTATE_180:
      this->mouseScaleX = 2.0f / this->format.screenWidth;
      this->mouseScaleY = 2.0f / this->format.screenHeight;
      w = this->format.screenWidth;
      h = this->format.screenHeight;
      break;

    case LG_ROTATE_90:
    case LG_ROTATE_270:
      this->mouseScaleX = 2.0f / this->format.screenHeight;
      this->mouseScaleY = 2.0f / this->format.screenWidth;
      w = this->format.screenHeight;
      h = this->format.screenWidth;
      break;

    default:
      DEBUG_UNREACHABLE();
  }

  switch((this->format.rotate + this->rotate) % LG_ROTATE_MAX)
  {
    case LG_ROTATE_0:
    case LG_ROTATE_180:
      egl_cursorSetSize(this->cursor,
        (this->mouseWidth  * (1.0f / w)) * this->scaleX,
        (this->mouseHeight * (1.0f / h)) * this->scaleY
      );
      break;

    case LG_ROTATE_90:
    case LG_ROTATE_270:
      egl_cursorSetSize(this->cursor,
        (this->mouseWidth  * (1.0f / w)) * this->scaleY,
        (this->mouseHeight * (1.0f / h)) * this->scaleX
      );
      break;
  }
}

static void egl_calc_mouse_state(struct Inst * this)
{
  if (this->showSwSurface)
  {
    egl_cursorSetState(
      this->cursor,
      this->cursorVisible,
      (((float)this->cursorX  * this->mouseScaleX) - 1.0f) * this->scaleX,
      (((float)this->cursorY  * this->mouseScaleY) - 1.0f) * this->scaleY,
      ((float)this->cursorHX * this->mouseScaleX) * this->scaleX,
      ((float)this->cursorHY * this->mouseScaleY) * this->scaleY
    );
    return;
  }

  if (!this->formatValid)
    return;

  switch((this->format.rotate + this->rotate) % LG_ROTATE_MAX)
  {
    case LG_ROTATE_0:
    case LG_ROTATE_180:
      egl_cursorSetState(
        this->cursor,
        this->cursorVisible,
        (((float)this->cursorX  * this->mouseScaleX) - 1.0f) * this->scaleX,
        (((float)this->cursorY  * this->mouseScaleY) - 1.0f) * this->scaleY,
        ((float)this->cursorHX * this->mouseScaleX) * this->scaleX,
        ((float)this->cursorHY * this->mouseScaleY) * this->scaleY
      );
      break;

    case LG_ROTATE_90:
    case LG_ROTATE_270:
      egl_cursorSetState(
        this->cursor,
        this->cursorVisible,
        (((float)this->cursorX  * this->mouseScaleX) - 1.0f) * this->scaleY,
        (((float)this->cursorY  * this->mouseScaleY) - 1.0f) * this->scaleX,
        ((float)this->cursorHX * this->mouseScaleX) * this->scaleY,
        ((float)this->cursorHY * this->mouseScaleY) * this->scaleX
      );
      break;
  }
}

static void egl_update_scale_type(struct Inst * this)
{
  int width, height;
  egl_getDesktopSize(this, &width, &height);

  switch (this->rotate)
  {
    case LG_ROTATE_0:
    case LG_ROTATE_180:
      break;

    case LG_ROTATE_90:
    case LG_ROTATE_270:
    {
      const int swap = width;
      width          = height;
      height         = swap;
      break;
    }
  }

  if (width == this->viewportWidth || height == this->viewportHeight)
    this->scaleType = EGL_DESKTOP_NOSCALE;
  else if (width > this->viewportWidth || height > this->viewportHeight)
    this->scaleType = EGL_DESKTOP_DOWNSCALE;
  else
    this->scaleType = EGL_DESKTOP_UPSCALE;
}

void egl_resetViewport(EGL * this)
{
  egl_stateViewport(0, 0, this->width, this->height);
}

static void egl_onResize(LG_Renderer * renderer, const int width, const int height, const double scale,
    const LG_RendererRect destRect, LG_RendererRotate rotate)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_stateCheckShared();

  this->width   = width * scale;
  this->height  = height * scale;
  this->uiScale = (float) scale;
  this->rotate  = rotate;

  this->destRect.x = destRect.x * scale;
  this->destRect.y = destRect.y * scale;
  this->destRect.w = destRect.w * scale;
  this->destRect.h = destRect.h * scale;

  egl_stateViewport(0, 0, this->width, this->height);

  if (destRect.valid)
  {
    this->translateX     = -1.0f + (((this->destRect.w / 2) + this->destRect.x) * 2) / (float)this->width;
    this->translateY     =  1.0f - (((this->destRect.h / 2) + this->destRect.y) * 2) / (float)this->height;
    this->scaleX         = (float)this->destRect.w / (float)this->width;
    this->scaleY         = (float)this->destRect.h / (float)this->height;
    this->viewportWidth  = this->destRect.w;
    this->viewportHeight = this->destRect.h;
  }

  egl_update_scale_type(this);
  egl_calc_mouse_size(this);

  this->splashRatio  = (float)width / (float)height;
  this->screenScaleX = 1.0f / this->width;
  this->screenScaleY = 1.0f / this->height;

  egl_calc_mouse_state(this);
  egl_update_pointer_scale(this);

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    this->desktopDamage[this->desktopDamageIdx].count = -1;
  });

  if (!egl_hdrOverlayResize(this->hdrOverlay, this->width, this->height))
    DEBUG_ERROR("Failed to resize the HDR overlay framebuffer");
  if (this->hdrCompose &&
      !egl_hdrComposeResize(this->hdrCompose, this->width, this->height))
    DEBUG_FATAL("Failed to resize the linear HDR composition framebuffer");

  egl_damageResize(this->damage, this->translateX, this->translateY, this->scaleX, this->scaleY);
  egl_desktopResize(this->desktop, this->width, this->height);
}

static bool egl_onFontUpdate(LG_Renderer * renderer)
{
  egl_stateInvalidate();
  return true;
}

static bool egl_onMouseShape(LG_Renderer * renderer, const LG_RendererCursor cursor,
    const int width, const int height,
    const int pitch, const uint8_t * data)
{
  struct Inst * this = UPCAST(struct Inst, renderer);

  if (!egl_cursorSetShape(this->cursor, cursor, width, height, pitch, data))
  {
    DEBUG_ERROR("Failed to update the cursor shape");
    return false;
  }

  this->mouseWidth  = width;
  this->mouseHeight = height;
  egl_calc_mouse_size(this);

  return true;
}

static void egl_onMouseColorTransform(LG_Renderer * renderer,
    const LGColorTransform * transform)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_cursorSetColorTransform(this->cursor, transform);
}

static void egl_onMouseWhiteLevel(LG_Renderer * renderer,
    uint32_t sdrWhiteLevel)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_cursorSetSDRWhiteLevel(this->cursor, sdrWhiteLevel);
}

static bool egl_onMouseEvent(LG_Renderer * renderer, const bool visible,
    int x, int y, const int hx, const int hy)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  this->cursorVisible = visible;
  this->cursorX       = x + hx;
  this->cursorY       = y + hy;
  this->cursorHX      = hx;
  this->cursorHY      = hy;
  egl_calc_mouse_state(this);
  return true;
}

static bool egl_updateHDRState(struct Inst * this, bool force)
{
  bool nativeHDR = false;
  app_getProp(LG_DS_NATIVE_HDR, &nativeHDR);
  const bool surfaceCompatible = this->format.hdrPQ ?
    (this->surfaceSupportsPQ &&
      egl_hdrComposeIsConfigured(this->hdrCompose)) :
    this->surfaceSupportsSCRGB;
  const bool useNativeHDR = !this->showSwSurface && this->format.hdr &&
    surfaceCompatible && nativeHDR && !app_getHDRDescFailed();

  LG_DSHDRWhiteLevels whiteLevels =
  {
    .pq    = 203,
    .scRGB = 80,
  };
  if (useNativeHDR && !app_getProp(LG_DS_HDR_WHITE_LEVELS, &whiteLevels))
    DEBUG_WARN("Display server did not provide native HDR white levels");
  const float referenceWhiteLevel = this->format.hdrPQ ?
    whiteLevels.pq : whiteLevels.scRGB;
  const bool stateChanged        = this->nativeHDR != useNativeHDR ||
    (useNativeHDR &&
      (this->nativeHDRPQ != this->format.hdrPQ ||
       this->nativeReferenceWhiteLevel != referenceWhiteLevel));

  if (!force && !stateChanged)
    return false;

  this->nativeHDR                 = useNativeHDR;
  this->nativeHDRPQ               = this->format.hdrPQ;
  this->nativeReferenceWhiteLevel = referenceWhiteLevel;
  this->linearHDRComposition      = useNativeHDR && this->format.hdrPQ;
  egl_hdrComposeSetActive(this->hdrCompose,
      this->linearHDRComposition);
  egl_desktopSetNativeHDR(this->desktop, useNativeHDR,
      this->linearHDRComposition);
  egl_hdrOverlaySetState(this->hdrOverlay, useNativeHDR,
      this->format.hdrPQ && !this->linearHDRComposition,
      referenceWhiteLevel);
  egl_damageSetHDRState(this->damage, useNativeHDR,
      referenceWhiteLevel);
  return true;
}

static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat format)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  memcpy(&this->format, &format, sizeof(LG_RendererFormat));
  this->formatValid = true;

  /* this event runs in a second thread so we need to init it here */
  if (unlikely(!this->frameContext))
  {
    static EGLint attrs[] = {
      EGL_CONTEXT_CLIENT_VERSION, 2,
      EGL_NONE
    };

    if (!(this->frameContext = eglCreateContext(this->display, this->configs, this->context, attrs)))
    {
      DEBUG_ERROR("Failed to create the frame context");
      return false;
    }

    if (!eglMakeCurrent(this->display, EGL_NO_SURFACE, EGL_NO_SURFACE, this->frameContext))
    {
      DEBUG_ERROR("Failed to make the frame context current");
      return false;
    }

    egl_stateInvalidate();
  }

  egl_stateCheckShared();

  egl_update_pointer_scale(this);

  // Track HDR state and warn if HDR frames arrive without any HDR path
  this->hdr = format.hdr;
  if (format.hdr)
  {
    bool nativeHDR = false;
    app_getProp(LG_DS_NATIVE_HDR, &nativeHDR);
    const bool surfaceCompatible = format.hdrPQ ?
      (this->surfaceSupportsPQ &&
        egl_hdrComposeIsConfigured(this->hdrCompose)) :
      this->surfaceSupportsSCRGB;
    if (!surfaceCompatible)
      DEBUG_WARN("HDR frames being displayed on an 8-bit EGL surface "
          "or an EGL surface incompatible with the source encoding");
  }

  egl_updateHDRState(this, true);

  egl_update_scale_type(this);

  if (!this->showSwSurface)
  {
    egl_damageSetup(this->damage, format.frameWidth, format.frameHeight);

    /* we need full screen damage when the format changes */
    INTERLOCKED_SECTION(this->desktopDamageLock, {
      this->desktopDamage[this->desktopDamageIdx].count = -1;
    });
  }

  return egl_desktopSetup(this->desktop, format);
}

static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame,
    int dmaFd, const FrameDamageRect * damageRects, int damageRectsCount,
    LG_RendererFrameToken frameToken, LG_FrameReleaseFn releaseFn,
    void * releaseOpaque, uint64_t releaseHandle)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_stateCheckShared();

  const uint64_t start      = nanotime();
  uint64_t       waitTimeNs = 0;
  if (unlikely(!egl_desktopUpdate(
          this->desktop, frame, frameToken, dmaFd,
          damageRects, damageRectsCount, &waitTimeNs,
          releaseFn, releaseOpaque, releaseHandle)))
  {
    DEBUG_INFO("Failed to to update the desktop");
    return false;
  }

  const uint64_t elapsed = nanotime() - start;

  /* Producer Copy already covers the interval before FrameBuffer::wp becomes
   * ready. Exclude that overlapping wait from the client Import stage. */
  app_setFrameImportTiming(
      elapsed > waitTimeNs ? elapsed - waitTimeNs : 0, waitTimeNs);

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    if (!this->showSwSurface)
    {
      struct DesktopDamage * damage =
        this->desktopDamage + this->desktopDamageIdx;
      if (unlikely(
          damage->count                    == -1 ||
          damageRectsCount                 == 0 ||
          damage->count + damageRectsCount >= LG_MAX_FRAME_DAMAGE_RECTS))
      {
        damage->count = -1;
      }
      else
      {
        memcpy(damage->rects + damage->count, damageRects,
            damageRectsCount * sizeof(FrameDamageRect));
        damage->count += damageRectsCount;
      }
      damage->frameToken = frameToken;
    }
  });

  return true;
}

static void egl_onFramePoll(LG_Renderer * renderer)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  if (!this->frameContext)
    return;

  egl_stateCheckShared();
  egl_desktopPoll(this->desktop);
}

static void debugCallback(GLenum source, GLenum type, GLuint id,
    GLenum severity, GLsizei length, const GLchar * message,
    const void * userParam)
{
  enum DebugLevel level = DEBUG_LEVEL_FIXME;
  switch (severity)
  {
    case GL_DEBUG_SEVERITY_HIGH:
      level = DEBUG_LEVEL_ERROR;
      break;
    case GL_DEBUG_SEVERITY_MEDIUM:
      level = DEBUG_LEVEL_WARN;
      break;
    case GL_DEBUG_SEVERITY_LOW:
    case GL_DEBUG_SEVERITY_NOTIFICATION:
      level = DEBUG_LEVEL_INFO;
      break;
  }

  const char * sourceName = "unknown";
  switch (source)
  {
    case GL_DEBUG_SOURCE_API:
      sourceName = "OpenGL API";
      break;
    case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
      sourceName = "window system";
      break;
    case GL_DEBUG_SOURCE_SHADER_COMPILER:
      sourceName = "shader compiler";
      break;
    case GL_DEBUG_SOURCE_THIRD_PARTY:
      sourceName = "third party";
      break;
    case GL_DEBUG_SOURCE_APPLICATION:
      sourceName = "application";
      break;
    case GL_DEBUG_SOURCE_OTHER:
      sourceName = "other";
      break;
  }

  const char * typeName = "unknown";
  switch (type)
  {
    case GL_DEBUG_TYPE_ERROR:
      typeName = "error";
      break;
    case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
      typeName = "deprecated behaviour";
      break;
    case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
      typeName = "undefined behaviour";
      break;
    case GL_DEBUG_TYPE_PORTABILITY:
      typeName = "portability";
      break;
    case GL_DEBUG_TYPE_PERFORMANCE:
      typeName = "performance";
      break;
    case GL_DEBUG_TYPE_MARKER:
      typeName = "marker";
      break;
    case GL_DEBUG_TYPE_PUSH_GROUP:
      typeName = "group pushing";
      break;
    case GL_DEBUG_TYPE_POP_GROUP:
      typeName = "group popping";
      break;
    case GL_DEBUG_TYPE_OTHER:
      typeName = "other";
      break;
  }

  DEBUG_PRINT(level, "GL message (source: %s, type: %s): %s", sourceName, typeName, message);
}

static void egl_configUI(void * opaque, int * id)
{
  struct Inst * this = opaque;
  egl_damageConfigUI(this->damage);
  igSeparator();
  egl_desktopConfigUI(this->desktop);
}

static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA)
{
  struct Inst * this = UPCAST(struct Inst, renderer);

  this->nativeWind = app_getEGLNativeWindow();
  if (!this->nativeWind)
  {
    DEBUG_ERROR("Failed to get EGL native window");
    return false;
  }

  this->display = app_getEGLDisplay();
  if (this->display == EGL_NO_DISPLAY)
  {
    DEBUG_ERROR("Failed to get EGL display");
    return false;
  }

  int maj, min;
  if (!eglInitialize(this->display, &maj, &min))
  {
    DEBUG_ERROR("Unable to initialize EGL");
    return false;
  }

  int maxSamples = 1;
  if (option_get_bool("egl", "multisample"))
  {
    if (app_getProp(LG_DS_MAX_MULTISAMPLE, &maxSamples) && maxSamples > 1)
    {
      if (maxSamples > 4)
        maxSamples = 4;

      DEBUG_INFO("Multisampling enabled, max samples: %d", maxSamples);
    }
  }

  // Probe for best available color depth: floating-point FP16, then fixed
  // 10-bit, then fixed 8-bit. Component type defaults to fixed-point in EGL,
  // so it must be requested explicitly for an scRGB-capable surface.
#ifndef EGL_COLOR_COMPONENT_TYPE_EXT
#define EGL_COLOR_COMPONENT_TYPE_EXT       0x3339
#define EGL_COLOR_COMPONENT_TYPE_FIXED_EXT 0x333A
#define EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT 0x333B
#endif
  static const struct
  {
    EGLint r, g, b, a, depth, componentType;
    bool pq, scRGB;
    const char * desc;
  } configs[] =
  {
    { 16, 16, 16,  0, 48, EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT,
      true, true, "FP16 (RGBA16F)" },
    { 10, 10, 10,  2, 32, EGL_COLOR_COMPONENT_TYPE_FIXED_EXT,
      true, false, "10-bit (RGBA10)" },
    {  8,  8,  8,  0, 24, EGL_COLOR_COMPONENT_TYPE_FIXED_EXT,
      false, false, "8-bit (RGBA8)" },
  };
  const char * eglExtensions = eglQueryString(this->display, EGL_EXTENSIONS);
  const bool hasFloatConfigs = eglExtensions &&
    util_hasGLExt(eglExtensions, "EGL_EXT_pixel_format_float");
  EGLint       num_config;
  const char * configDesc = NULL;
  int          chosen     = -1;

  for (int i = 0; i < (int)ARRAY_LENGTH(configs); ++i)
  {
    if (configs[i].componentType == EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT &&
        !hasFloatConfigs)
      continue;

    EGLint attr[23];
    int ai = 0;
#define EGL_CONFIG_ATTR(name, value) \
    do { attr[ai++] = (name); attr[ai++] = (value); } while (0)
    EGL_CONFIG_ATTR(EGL_RED_SIZE       , configs[i].r);
    EGL_CONFIG_ATTR(EGL_GREEN_SIZE     , configs[i].g);
    EGL_CONFIG_ATTR(EGL_BLUE_SIZE      , configs[i].b);
    EGL_CONFIG_ATTR(EGL_ALPHA_SIZE     , configs[i].a);
    EGL_CONFIG_ATTR(EGL_BUFFER_SIZE    , configs[i].depth);
    if (hasFloatConfigs)
      EGL_CONFIG_ATTR(EGL_COLOR_COMPONENT_TYPE_EXT, configs[i].componentType);
    EGL_CONFIG_ATTR(EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT);
    EGL_CONFIG_ATTR(EGL_SAMPLE_BUFFERS , maxSamples > 0 ? 1 : 0);
    EGL_CONFIG_ATTR(EGL_SAMPLES        , maxSamples);
    attr[ai] = EGL_NONE;
#undef EGL_CONFIG_ATTR

    if (eglChooseConfig(this->display, attr, &this->configs, 1, &num_config) &&
        num_config > 0)
    {
      configDesc = configs[i].desc;
      chosen     = i;
      break;
    }
  }

  if (chosen < 0)
  {
    DEBUG_ERROR("Failed to choose any EGL config");
    return false;
  }

  this->surfaceSupportsPQ    = configs[chosen].pq;
  this->surfaceSupportsSCRGB = configs[chosen].scRGB;
  this->captureFormat        = chosen == 0 ? LG_CAPTURE_RGBA32F :
    chosen == 1 ? LG_CAPTURE_RGB10_A2 : LG_CAPTURE_RGBA8;
  DEBUG_INFO("EGL config: %s%s", configDesc,
      this->surfaceSupportsPQ ? " (HDR capable)" : "");

  const EGLint surfattr[] =
  {
    EGL_RENDER_BUFFER, this->opt.doubleBuffer ? EGL_BACK_BUFFER : EGL_SINGLE_BUFFER,
    EGL_NONE
  };

  this->surface = eglCreateWindowSurface(this->display, this->configs, this->nativeWind, surfattr);
  if (this->surface == EGL_NO_SURFACE)
  {
    // On Nvidia proprietary drivers on Wayland, specifying EGL_RENDER_BUFFER can cause
    // window creation to fail, so we try again without it.
    this->surface = eglCreateWindowSurface(this->display, this->configs, this->nativeWind, NULL);
    if (this->surface == EGL_NO_SURFACE)
    {
      DEBUG_ERROR("Failed to create EGL surface (eglError: 0x%x)", eglGetError());
      return false;
    }
    else
      DEBUG_WARN("EGL surface creation with EGL_RENDER_BUFFER failed, "
        "egl:doubleBuffer setting may not be respected");
  }

  const char * client_exts = eglQueryString(this->display, EGL_EXTENSIONS);
  if (!client_exts)
  {
    DEBUG_ERROR("Failed to query EGL_EXTENSIONS");
    return false;
  }

  bool debug = option_get_bool("egl", "debug");
  EGLint ctxattr[5];
  int ctxidx = 0;

  ctxattr[ctxidx++] = EGL_CONTEXT_CLIENT_VERSION;
  ctxattr[ctxidx++] = 2;

  if (maj > 1 || (maj == 1 && min >= 5))
  {
    ctxattr[ctxidx++] = EGL_CONTEXT_OPENGL_DEBUG;
    ctxattr[ctxidx++] = debug ? EGL_TRUE : EGL_FALSE;
  }
  else if (util_hasGLExt(client_exts, "EGL_KHR_create_context"))
  {
    ctxattr[ctxidx++] = EGL_CONTEXT_FLAGS_KHR;
    ctxattr[ctxidx++] = debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0;
  }
  else if (debug)
    DEBUG_WARN("Cannot create debug contexts before EGL 1.5 without EGL_KHR_create_context");

  ctxattr[ctxidx] = EGL_NONE;

  this->context = eglCreateContext(this->display, this->configs, EGL_NO_CONTEXT, ctxattr);
  if (this->context == EGL_NO_CONTEXT)
  {
    DEBUG_ERROR("Failed to create EGL context (eglError: 0x%x)", eglGetError());
    return false;
  }

  EGLint rb = 0;
  eglQuerySurface(this->display, this->surface, EGL_RENDER_BUFFER, &rb);
  switch(rb)
  {
    case EGL_SINGLE_BUFFER:
      DEBUG_INFO("Single buffer mode");
      break;

    case EGL_BACK_BUFFER:
      DEBUG_INFO("Back buffer mode");
      break;

    default:
      DEBUG_WARN("Unknown render buffer mode: %d", rb);
      break;
  }

  eglMakeCurrent(this->display, this->surface, this->surface, this->context);
  egl_stateInvalidate();
  const char * gl_exts = (const char *)glGetString(GL_EXTENSIONS);
  if (!gl_exts)
  {
    DEBUG_ERROR("Failed to query GL_EXTENSIONS");
    return false;
  }

  const char * vendor  = (const char *)glGetString(GL_VENDOR);
  if (!vendor)
  {
    DEBUG_ERROR("Failed to query GL_VENDOR");
    return false;
  }

  DEBUG_INFO("EGL     : %d.%d", maj, min);
  DEBUG_INFO("Vendor  : %s", vendor);
  DEBUG_INFO("Renderer: %s", glGetString(GL_RENDERER));
  DEBUG_INFO("Version : %s", glGetString(GL_VERSION ));
  DEBUG_INFO("EGL APIs: %s", eglQueryString(this->display, EGL_CLIENT_APIS));

  if (debug)
  {
    DEBUG_INFO("EGL Exts: %s", client_exts);
    DEBUG_INFO("GL Exts : %s", gl_exts);
  }

  GLint esMaj, esMin;
  glGetIntegerv(GL_MAJOR_VERSION, &esMaj);
  glGetIntegerv(GL_MINOR_VERSION, &esMin);

  if (!util_hasGLExt(gl_exts, "GL_EXT_texture_format_BGRA8888"))
  {
    DEBUG_ERROR("GL_EXT_texture_format_BGRA8888 is needed to use EGL backend");
    return false;
  }

  if (this->surfaceSupportsPQ &&
      !util_hasGLExt(gl_exts, "GL_EXT_color_buffer_half_float") &&
      !util_hasGLExt(gl_exts, "GL_EXT_color_buffer_float"))
  {
    DEBUG_WARN("Half-float render targets are unavailable; native PQ "
        "composition disabled");
    this->surfaceSupportsPQ = false;
  }

  this->hasBufferAge = util_hasGLExt(client_exts, "EGL_EXT_buffer_age");
  if (!this->hasBufferAge)
    DEBUG_WARN("GL_EXT_buffer_age is not supported, will not perform as well.");

  if (this->hasBufferAge && option_get_bool("egl", "noBufferAge"))
  {
    DEBUG_WARN("egl:noBufferAge specified, disabling buffer age.");
    this->hasBufferAge = false;
  }

  this->noSwapDamage = option_get_bool("egl", "noSwapDamage");
  if (this->noSwapDamage)
    DEBUG_WARN("egl:noSwapDamage specified, disabling swap buffers with damage.");

  this->scalePointer = option_get_bool("egl", "scalePointer");

  if (!g_egl_dynProcs.glEGLImageTargetTexture2DOES)
    DEBUG_INFO("glEGLImageTargetTexture2DOES unavilable, DMA support disabled");
  else if (!g_egl_dynProcs.eglCreateImage || !g_egl_dynProcs.eglDestroyImage)
    DEBUG_INFO("eglCreateImage or eglDestroyImage unavailable, DMA support disabled");
  else if (!util_hasGLExt(client_exts, "EGL_EXT_image_dma_buf_import"))
    DEBUG_INFO("EGL_EXT_image_dma_buf_import unavailable, DMA support disabled");
  else if ((maj < 1 || (maj == 1 && min < 5)) && !util_hasGLExt(client_exts, "EGL_KHR_image_base"))
    DEBUG_INFO("Need EGL 1.5+ or EGL_KHR_image_base for eglCreateImage(KHR)");
  else
    this->dmaSupport = true;

  if (!this->dmaSupport)
  {
    useDMA = false;
    if (!util_hasGLExt(gl_exts, "GL_EXT_buffer_storage"))
    {
      DEBUG_ERROR("GL_EXT_buffer_storage is needed to use EGL backend");
      return false;
    }
  }

  if (debug)
  {
    if ((esMaj > 3 || (esMaj == 3 && esMin >= 2)) && g_egl_dynProcs.glDebugMessageCallback)
    {
      g_egl_dynProcs.glDebugMessageCallback(debugCallback, NULL);
      DEBUG_INFO("Using debug message callback from OpenGL ES 3.2+");
    }
    else if (util_hasGLExt(gl_exts, "GL_KHR_debug") && g_egl_dynProcs.glDebugMessageCallbackKHR)
    {
      g_egl_dynProcs.glDebugMessageCallbackKHR(debugCallback, NULL);
      DEBUG_INFO("Using debug message callback from GL_KHR_debug");
    }
    else
      DEBUG_INFO("Debug message callback not supported");
  }
  else
    DEBUG_INFO("Debug messages disabled, enable with egl:debug=true");

  eglSwapInterval(this->display, this->opt.vsync ? 1 : 0);

  if (!egl_desktopInit(this, &this->desktop, this->display, useDMA, MAX_ACCUMULATED_DAMAGE))
  {
    DEBUG_ERROR("Failed to initialize the desktop");
    return false;
  }

  if (!egl_cursorInit(&this->cursor))
  {
    DEBUG_ERROR("Failed to initialize the cursor");
    return false;
  }

  if (!egl_damageInit(&this->damage))
  {
    DEBUG_ERROR("Failed to initialize the damage display");
    return false;
  }

  if (!egl_hdrOverlayInit(&this->hdrOverlay))
  {
    DEBUG_ERROR("Failed to initialize the HDR overlay");
    return false;
  }

  if (this->surfaceSupportsPQ &&
      !egl_hdrComposeInit(&this->hdrCompose))
  {
    DEBUG_WARN("Native PQ output disabled: failed to initialize linear "
        "HDR composition");
    this->surfaceSupportsPQ = false;
  }

  if (!ImGui_ImplOpenGL3_Init("#version 300 es"))
  {
    DEBUG_ERROR("Failed to initialize ImGui");
    return false;
  }
  egl_stateInvalidate();

  app_overlayConfigRegister("EGL", egl_configUI, this);

  this->imgui = true;
  return true;
}

inline static EGLint egl_bufferAge(struct Inst * this)
{
  if (!this->hasBufferAge)
    return 0;

  EGLint result;
  if (eglQuerySurface(this->display, this->surface, EGL_BUFFER_AGE_EXT, &result) == EGL_FALSE)
  {
    DEBUG_ERROR("eglQuerySurface(EGL_BUFFER_AGE_EXT) failed");
    return 0;
  }
  return result;
}

static FrameDamageRect egl_expandDesktopDamage(
    const struct Inst * this, const FrameDamageRect * rect)
{
  int width, height;
  egl_getDesktopSize(this, &width, &height);

  const uint32_t x = rect->x > DESKTOP_DAMAGE_MARGIN ?
    rect->x - DESKTOP_DAMAGE_MARGIN : 0;
  const uint32_t y = rect->y > DESKTOP_DAMAGE_MARGIN ?
    rect->y - DESKTOP_DAMAGE_MARGIN : 0;
  const uint32_t right = (uint32_t)min((uint64_t)width,
    (uint64_t)rect->x + rect->width + DESKTOP_DAMAGE_MARGIN);
  const uint32_t bottom = (uint32_t)min((uint64_t)height,
    (uint64_t)rect->y + rect->height + DESKTOP_DAMAGE_MARGIN);

  return (FrameDamageRect) {
    .x      = x,
    .y      = y,
    .width  = right  > x ? right  - x : 0,
    .height = bottom > y ? bottom - y : 0,
  };
}

static int egl_clipSurfaceDamage(
    struct Rect * damage, int count, int width, int height)
{
  int out = 0;
  for (int i = 0; i < count; ++i)
  {
    const int64_t x2 = (int64_t)damage[i].x + damage[i].w;
    const int64_t y2 = (int64_t)damage[i].y + damage[i].h;
    const int x1 = clamp(damage[i].x, 0, width);
    const int y1 = clamp(damage[i].y, 0, height);
    const int right  = (int)clamp(x2, 0, width);
    const int bottom = (int)clamp(y2, 0, height);
    if (right <= x1 || bottom <= y1)
      continue;

    damage[out++] = (struct Rect) {
      .x = x1,
      .y = y1,
      .w = right  - x1,
      .h = bottom - y1,
    };
  }

  return out;
}

static int egl_mergeSurfaceDamage(struct Rect * damage, int count)
{
  if (count <= 1)
    return count;

  FrameDamageRect rects[count];
  for (int i = 0; i < count; ++i)
  {
    rects[i] = (FrameDamageRect) {
      .x      = damage[i].x,
      .y      = damage[i].y,
      .width  = damage[i].w,
      .height = damage[i].h,
    };
  }

  count = rectsMergeOverlapping(rects, count);
  for (int i = 0; i < count; ++i)
  {
    damage[i] = (struct Rect) {
      .x = rects[i].x,
      .y = rects[i].y,
      .w = rects[i].width,
      .h = rects[i].height,
    };
  }

  return count;
}

static void egl_requeueDesktopDamage(
    struct Inst * this, const struct DesktopDamage * damage)
{
  if (damage->count == 0)
    return;

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    struct DesktopDamage * pending =
      this->desktopDamage + this->desktopDamageIdx;

    if (damage->count < 0 || pending->count < 0 ||
        damage->count + pending->count >= LG_MAX_FRAME_DAMAGE_RECTS)
      pending->count = -1;
    else
    {
      memcpy(pending->rects + pending->count, damage->rects,
          damage->count * sizeof(FrameDamageRect));
      pending->count += damage->count;
    }

    if (pending->frameToken == LG_RENDERER_FRAME_TOKEN_NONE)
      pending->frameToken = damage->frameToken;
  });
}

inline static void renderLetterBox(struct Inst * this)
{
  bool hLB = this->destRect.x > 0;
  bool vLB = this->destRect.y > 0;

  if (hLB || vLB)
  {
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    egl_stateScissor(true);

    if (hLB)
    {
      // left
      glScissor(0, 0, this->destRect.x, this->height);
      glClear(GL_COLOR_BUFFER_BIT);

      // right
      float x2 = this->destRect.x + this->destRect.w;
      glScissor(x2, 0, this->width - x2, this->height);
      glClear(GL_COLOR_BUFFER_BIT);
    }

    if (vLB)
    {
      // top
      glScissor(0, this->height - this->destRect.y,
          this->width, this->destRect.y);
      glClear(GL_COLOR_BUFFER_BIT);

      // bottom
      int y2 = this->destRect.y + this->destRect.h;
      glScissor(0, 0, this->width, this->height - y2);
      glClear(GL_COLOR_BUFFER_BIT);
    }

    egl_stateScissor(false);
  }
}

static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
    LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow,
    void (*preSwap)(void * udata), void * udata,
    LG_RendererFrameTiming * timing)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  *timing = (LG_RendererFrameTiming) {};

  const uint64_t setupStart = nanotime();
  egl_stateCheckShared();
  EGLint     bufferAge       = egl_bufferAge(this);
  const bool hdrStateChanged = egl_updateHDRState(this, false);
  bool       mapCursorHDR;
  float      mapCursorGain;
  float      mapCursorContentPeak;
  egl_desktopGetHDRMapping(this->desktop, &mapCursorHDR,
      &mapCursorGain, &mapCursorContentPeak);
  egl_cursorSetHDRState(this->cursor,
      this->format.hdr && !this->showSwSurface,
      this->nativeHDR, mapCursorHDR,
      this->format.hdrPQ, mapCursorGain, mapCursorContentPeak);
  bool renderAll = hdrStateChanged ||
                   invalidateWindow || this->hadOverlay ||
                   bufferAge <= 0 || bufferAge > MAX_BUFFER_AGE;

  int desktopWidth, desktopHeight;
  egl_getDesktopSize(this, &desktopWidth, &desktopHeight);

  bool                   hasOverlay      = false;
  struct CursorState     cursorState     = { .visible = false };
  struct DesktopDamage   noDesktopDamage = {};
  struct DesktopDamage * desktopDamage   = NULL;

  struct DamageRects * accumulated = (struct DamageRects *)alloca(
    sizeof(struct DamageRects) +
    MAX_ACCUMULATED_DAMAGE * sizeof(struct FrameDamageRect)
  );
  accumulated->count = 0;

  INTERLOCKED_SECTION(this->desktopDamageLock, {
    struct DesktopDamage * pendingDamage =
      this->desktopDamage + this->desktopDamageIdx;
    const bool damageQueued =
      pendingDamage->frameToken == LG_RENDERER_FRAME_TOKEN_NONE ||
      pendingDamage->frameToken <= frameTokenLimit;
    const int historyOffset = damageQueued ? 0 : 1;

    if (likely(!renderAll))
    {
      for (int i = 0; i < bufferAge; ++i)
      {
        struct DesktopDamage * damage = this->desktopDamage +
          IDX_AGO(this->desktopDamageIdx, i + historyOffset,
              DESKTOP_DAMAGE_COUNT);

        if (unlikely(damage->count < 0))
        {
          renderAll = true;
          break;
        }

        for (int j = 0; j < damage->count; ++j)
        {
          const FrameDamageRect rect =
            egl_expandDesktopDamage(this, damage->rects + j);
          if (rect.width && rect.height)
            accumulated->rects[accumulated->count++] = rect;
        }
      }
    }

    if (damageQueued)
    {
      desktopDamage = pendingDamage;
      this->desktopDamageIdx =
        (this->desktopDamageIdx + 1) % DESKTOP_DAMAGE_COUNT;
      this->desktopDamage[this->desktopDamageIdx].frameToken =
        LG_RENDERER_FRAME_TOKEN_NONE;
      this->desktopDamage[this->desktopDamageIdx].count = 0;
    }
    else
      desktopDamage = &noDesktopDamage;
  });

  if (hdrStateChanged)
    desktopDamage->count = -1;

  if (likely(!renderAll))
  {
    double matrix[6];
    egl_screenToDesktopMatrix(matrix,
        desktopWidth, desktopHeight,
        this->translateX, this->translateY, this->scaleX, this->scaleY, rotate,
        this->width, this->height);

    for (int i = 0; i < bufferAge; ++i)
    {
      int idx   = IDX_AGO(this->overlayHistoryIdx, i, DESKTOP_DAMAGE_COUNT);
      int count = this->overlayHistoryCount[idx];
      struct Rect * damage = this->overlayHistory[idx];

      if (unlikely(count < 0))
      {
        renderAll = true;
        break;
      }

      for (int j = 0; j < count; ++j)
        accumulated->count += egl_screenToDesktop(
          accumulated->rects + accumulated->count, matrix, damage + j,
          desktopWidth, desktopHeight
        );
    }

    accumulated->count = rectsMergeOverlapping(accumulated->rects,
        accumulated->count);
  }
  ++this->overlayHistoryIdx;

  bool                  fullFrame             = false;
  const bool            linearHDRComposition  = egl_hdrComposeBegin(
      this->hdrCompose);
  bool                  deferredLogicalCursor = false;
  const bool            haveDesktop           = likely(
      this->destRect.w > 0 && this->destRect.h > 0);
  bool                  desktopRendered    = false;
  const uint64_t        desktopStart       = nanotime();
  uint64_t              effectsTime        = 0;
  LG_RendererFrameToken frameToken         = LG_RENDERER_FRAME_TOKEN_NONE;
  LG_RendererFrameToken renderedFrameToken = LG_RENDERER_FRAME_TOKEN_NONE;

  timing->setupTime = desktopStart - setupStart;
  if (haveDesktop)
  {
    desktopRendered = egl_desktopRender(this->desktop,
        this->destRect.w, this->destRect.h,
        this->translateX, this->translateY,
        this->scaleX    , this->scaleY    ,
        this->scaleType , rotate, renderAll ? NULL : accumulated,
        desktopDamage->frameToken, frameTokenLimit, &fullFrame,
        &frameToken, &renderedFrameToken, &effectsTime,
        egl_hdrComposeGetFramebuffer(this->hdrCompose));
  }
  const uint64_t composeStart = nanotime();
  if (haveDesktop)
  {
    const uint64_t totalDesktopTime = composeStart - desktopStart;
    timing->effectsTime = effectsTime;
    timing->desktopTime = totalDesktopTime > effectsTime ?
      totalDesktopTime - effectsTime : 0;
  }
  else
    timing->setupTime += composeStart - desktopStart;

  const bool frameConsumed = desktopRendered &&
    frameToken != LG_RENDERER_FRAME_TOKEN_NONE;
  timing->frameToken = frameConsumed ?
    frameToken : LG_RENDERER_FRAME_TOKEN_NONE;

  if (haveDesktop)
  {
    if (desktopRendered)
    {
      cursorState = egl_cursorRender(this->cursor,
          ((this->showSwSurface ? LG_ROTATE_0 : this->format.rotate) + rotate) %
            LG_ROTATE_MAX,
          this->width, this->height,
          linearHDRComposition, &deferredLogicalCursor);
    }
    else
      hasOverlay = true;
  }

  /* Preserve the full update in the damage history so buffer-age repair still
   * redraws older swapchain buffers correctly. */
  if (fullFrame)
    desktopDamage->count = -1;

  /* A texture slot can be published before its matching damage and timing
   * record. If this render could not consume that slot, retain the damage so
   * it is merged into the next drawable frame instead of retiring it. */
  const bool damageRendered = desktopRendered &&
    (desktopDamage->frameToken == LG_RENDERER_FRAME_TOKEN_NONE ||
     desktopDamage->frameToken == renderedFrameToken ||
     ((renderAll || fullFrame) &&
      renderedFrameToken != LG_RENDERER_FRAME_TOKEN_NONE &&
      renderedFrameToken > desktopDamage->frameToken));
  if (!damageRendered)
    egl_requeueDesktopDamage(this, desktopDamage);

  renderLetterBox(this);

  hasOverlay |=
    egl_damageRender(
        this->damage, rotate,
        frameConsumed || (this->showSwSurface && damageRendered) ?
          desktopDamage : NULL) |
    invalidateWindow;

  /* The diagnostics being displayed must not contribute to Compose. */
  timing->composeTime = nanotime() - composeStart;

  struct Rect damage[LG_MAX_FRAME_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2];
  int damageIdx = app_renderOverlay(damage, MAX_OVERLAY_RECTS);
  if (unlikely(damageIdx != 0))
  {
    if (damageIdx == -1)
      hasOverlay = true;

    const bool hdrOverlay  = egl_hdrOverlayBegin(
        this->hdrOverlay, damage, damageIdx);
    const bool drawOverlay = !this->nativeHDR || hdrOverlay;
    ImGui_ImplOpenGL3_NewFrame();
    if (drawOverlay)
    {
      /* ImGui uploads atlas updates from CPU memory. The streamed desktop
       * path may have left its pixel unpack buffer bound. */
      egl_stateBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
      ImGui_ImplOpenGL3_RenderDrawData(igGetDrawData());
      egl_stateInvalidate();
    }
    if (hdrOverlay)
      egl_hdrOverlayEnd(this->hdrOverlay, damage, damageIdx,
          egl_hdrComposeGetFramebuffer(this->hdrCompose));

    for (int i = 0; i < damageIdx; ++i)
      damage[i].y = this->height - damage[i].y - damage[i].h;
  }
  const uint64_t postOverlayStart = nanotime();

  if (likely(damageIdx >= 0 && cursorState.visible))
    damage[damageIdx++] = cursorState.rect;

  int overlayHistoryIdx = this->overlayHistoryIdx % DESKTOP_DAMAGE_COUNT;
  if (unlikely(hasOverlay))
    this->overlayHistoryCount[overlayHistoryIdx] = -1;
  else
  {
    if (unlikely(damageIdx > 0))
      memcpy(this->overlayHistory[overlayHistoryIdx],
          damage, damageIdx * sizeof(struct Rect));
    this->overlayHistoryCount[overlayHistoryIdx] = damageIdx;
  }

  if (unlikely(!hasOverlay && !this->hadOverlay))
  {
    if (this->cursorLast.visible)
      damage[damageIdx++] = this->cursorLast.rect;

    if (desktopDamage->count == -1)
      // -1 damage count means invalidating entire window.
      damageIdx = 0;
    else
    {
      double matrix[6];
      egl_desktopToScreenMatrix(matrix,
          desktopWidth, desktopHeight,
          this->translateX, this->translateY, this->scaleX, this->scaleY, rotate,
          this->width, this->height);

      for (int i = 0; i < desktopDamage->count; ++i)
      {
        const FrameDamageRect rect =
          egl_expandDesktopDamage(this, desktopDamage->rects + i);
        if (rect.width && rect.height)
          damage[damageIdx++] = egl_desktopToScreen(matrix, &rect);
      }
    }
  }
  else
    damageIdx = 0;

  if (damageIdx > 0)
    damageIdx = egl_clipSurfaceDamage(
      damage, damageIdx, this->width, this->height);

  struct Rect * encodeDamage      = damage;
  int           encodeDamageCount =
    linearHDRComposition && renderAll ? 0 : damageIdx;
  if (linearHDRComposition && encodeDamageCount > 0)
  {
    const int capacity = damageIdx + accumulated->count;
    encodeDamage       = alloca(capacity * sizeof(*encodeDamage));
    memcpy(encodeDamage, damage, damageIdx * sizeof(*encodeDamage));
    encodeDamageCount  = damageIdx;

    double matrix[6];
    egl_desktopToScreenMatrix(matrix,
        desktopWidth, desktopHeight,
        this->translateX, this->translateY, this->scaleX, this->scaleY, rotate,
        this->width, this->height);
    for (int i = 0; i < accumulated->count; ++i)
      encodeDamage[encodeDamageCount++] =
        egl_desktopToScreen(matrix, accumulated->rects + i);

    encodeDamageCount = egl_clipSurfaceDamage(
        encodeDamage, encodeDamageCount, this->width, this->height);
    encodeDamageCount = egl_mergeSurfaceDamage(
        encodeDamage, encodeDamageCount);
  }

  egl_hdrComposeEnd(this->hdrCompose, encodeDamage, encodeDamageCount);
  if (deferredLogicalCursor && cursorState.visible)
    egl_cursorRender(this->cursor,
        ((this->showSwSurface ? LG_ROTATE_0 : this->format.rotate) + rotate) %
          LG_ROTATE_MAX,
        this->width, this->height, false, NULL);
  this->hadOverlay = hasOverlay;
  this->cursorLast = cursorState;

  preSwap(udata);

  const uint64_t composeEnd = nanotime();
  timing->composeTime += composeEnd - postOverlayStart;

  const bool     swapResult = app_eglSwapBuffers(
      this->display, this->surface, damage,
      this->noSwapDamage ? 0 : damageIdx, timing->frameToken,
      &timing->swapTime, &timing->presentTracked);

  if (!swapResult)
    DEBUG_ERROR("Failed to swap EGL buffers (eglError: 0x%x)", eglGetError());

  return swapResult;
}

static bool egl_capture(LG_Renderer * renderer, LG_RendererCapture * capture)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  if (!capture || this->width <= 0 || this->height <= 0)
    return false;

  size_t bytesPerPixel;
  GLenum dataType;
  switch (this->captureFormat)
  {
    case LG_CAPTURE_RGBA8:
      bytesPerPixel = 4;
      dataType      = GL_UNSIGNED_BYTE;
      break;

    case LG_CAPTURE_RGB10_A2:
      bytesPerPixel = 4;
      dataType      = GL_UNSIGNED_INT_2_10_10_10_REV;
      break;

    case LG_CAPTURE_RGBA32F:
      bytesPerPixel = sizeof(float) * 4;
      dataType      = GL_FLOAT;
      break;

    default:
      return false;
  }

  if ((size_t)this->width > SIZE_MAX / bytesPerPixel ||
      (size_t)this->height >
        SIZE_MAX / ((size_t)this->width * bytesPerPixel))
    return false;

  const size_t stride   = (size_t)this->width * bytesPerPixel;
  const size_t dataSize = stride * this->height;
  void * data = malloc(dataSize);
  if (!data)
    return false;

  while (glGetError() != GL_NO_ERROR)
    ;
  glPixelStorei(GL_PACK_ALIGNMENT, 1);
  glReadPixels(0, 0, this->width, this->height, GL_RGBA, dataType, data);
  if (glGetError() != GL_NO_ERROR)
  {
    free(data);
    DEBUG_ERROR("Failed to read the composed EGL framebuffer");
    return false;
  }

  *capture = (LG_RendererCapture) {
    .width     = this->width,
    .height    = this->height,
    .stride    = stride,
    .dataSize  = dataSize,
    .format    = this->captureFormat,
    .hdr       = this->format.hdr,
    .hdrPQ     = this->format.hdrPQ,
    .nativeHDR = this->nativeHDR,
    .data      = data,
  };
  return true;
}

static void * egl_createTexture(LG_Renderer * renderer,
  int width, int height, uint8_t * data)
{
  egl_stateCheckShared();

  GLuint tex;
  glGenTextures(1, &tex);
  egl_stateBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
  egl_stateBindTexture(0, GL_TEXTURE_2D, tex);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  glPixelStorei(GL_UNPACK_ROW_LENGTH, width);
  glTexImage2D(
      GL_TEXTURE_2D,
      0,
      GL_RGBA,
      width,
      height,
      0,
      GL_RGBA,
      GL_UNSIGNED_BYTE,
      data);

  return (void*)(intptr_t)tex;
}

static void egl_freeTexture(LG_Renderer * renderer, void * texture)
{
  GLuint tex = (GLuint)(intptr_t)texture;
  glDeleteTextures(1, &tex);
  egl_stateInvalidateShared();
}

static void egl_swSurfaceConfigure(LG_Renderer * renderer,
    int width, int height)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_stateCheckShared();
  this->swSurfaceWidth  = width;
  this->swSurfaceHeight = height;
  egl_desktopSwSurfaceConfigure(this->desktop, width, height);

  if (this->showSwSurface && width > 0 && height > 0)
  {
    egl_update_scale_type(this);
    egl_calc_mouse_size(this);
    egl_calc_mouse_state(this);
    egl_update_pointer_scale(this);
    egl_damageSetup(this->damage, width, height);
    egl_invalidateDesktopDamage(this);
  }
}

static void egl_swSurfaceDrawFill(LG_Renderer * renderer,
    int x, int y, int width, int height, uint32_t color)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_stateCheckShared();
  egl_desktopSwSurfaceDrawFill(
      this->desktop, x, y, width, height, color);
  egl_addSwSurfaceDamage(this, x, y, width, height);
}

static void egl_swSurfaceDrawBitmap(LG_Renderer * renderer,
    int x, int y, int width, int height, int stride, uint8_t * data,
    bool topDown)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  egl_stateCheckShared();
  egl_desktopSwSurfaceDrawBitmap(
      this->desktop, x, y, width, height, stride, data, topDown);
  egl_addSwSurfaceDamage(this, x, y, width, height);
}

static void egl_swSurfaceShow(LG_Renderer * renderer, bool show)
{
  struct Inst * this = UPCAST(struct Inst, renderer);
  INTERLOCKED_SECTION(this->desktopDamageLock, {
    this->showSwSurface = show;
    struct DesktopDamage * damage =
      this->desktopDamage + this->desktopDamageIdx;
    damage->frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
    damage->count      = -1;
  });

  int width, height;
  egl_getDesktopSize(this, &width, &height);
  if (width > 0 && height > 0)
    egl_damageSetup(this->damage, width, height);

  egl_update_scale_type(this);
  egl_calc_mouse_size(this);
  egl_calc_mouse_state(this);
  egl_update_pointer_scale(this);
  egl_desktopSwSurfaceShow(this->desktop, show);
}

struct LG_RendererOps LGR_EGL =
{
  .getName               = egl_getName,
  .setup                 = egl_setup,
  .create                = egl_create,
  .initialize            = egl_initialize,
  .deinitialize          = egl_deinitialize,
  .supports              = egl_supports,
  .getInterop            = egl_getInterop,
  .onRestart             = egl_onRestart,
  .onResize              = egl_onResize,
  .onFontUpdate          = egl_onFontUpdate,
  .onMouseShape          = egl_onMouseShape,
  .onMouseColorTransform = egl_onMouseColorTransform,
  .onMouseWhiteLevel     = egl_onMouseWhiteLevel,
  .onMouseEvent          = egl_onMouseEvent,
  .onFrameFormat         = egl_onFrameFormat,
  .onFrame               = egl_onFrame,
  .onFramePoll           = egl_onFramePoll,
  .renderStartup         = egl_renderStartup,
  .render                = egl_render,
  .capture               = egl_capture,
  .createTexture         = egl_createTexture,
  .freeTexture           = egl_freeTexture,

  .swSurfaceConfigure    = egl_swSurfaceConfigure,
  .swSurfaceDrawFill     = egl_swSurfaceDrawFill,
  .swSurfaceDrawBitmap   = egl_swSurfaceDrawBitmap,
  .swSurfaceShow         = egl_swSurfaceShow
};
