/**
 * 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 "desktop.h"
#include "state.h"
#include "framebuffer.h"
#include "common/debug.h"
#include "common/option.h"
#include "common/locking.h"
#include "common/time.h"

#include "app.h"
#include "texture.h"
#include "texture_buffer.h"
#include "shader.h"
#include "desktop_rects.h"
#include "cimgui.h"

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

// these headers are auto generated by cmake
#include "desktop.vert.h"
#include "desktop_rgb.frag.h"
#include "desktop_rgb.def.h"

#include "postprocess.h"
#include "filters.h"

enum
{
  HDR_PEAK_LUMINANCE_MIN     = 80,
  HDR_PEAK_LUMINANCE_DEFAULT = 10000,
  HDR_PEAK_LUMINANCE_MAX     = 10000
};

struct DesktopShader
{
  EGL_Shader * shader;
  EGL_Uniform * uTransform;
  EGL_Uniform * uDesktopSize;
  EGL_Uniform * uScaleAlgo;
  EGL_Uniform * uNVGain;
  EGL_Uniform * uCBMode;
  EGL_Uniform * uIsHDR;
  EGL_Uniform * uMapHDRtoSDR;
  EGL_Uniform * uMapHDRGain;
  EGL_Uniform * uMapHDRContentPeak;
  EGL_Uniform * uMapHDRPQ;
  EGL_Uniform * uOutputHDRLinear;
};

struct EGL_Desktop
{
  EGL * egl;
  EGLDisplay * display;

  EGL_Texture          * texture;
  struct DesktopShader   shader;
  EGL_DesktopRects     * mesh;
  GLfloat                matrix[6];

  // internals
  int               width, height;
  bool              hdr;
  bool              hdrPQ;
  LG_RendererRotate rotate;

  bool          useSwSurface;
  int           swSurfaceWidth, swSurfaceHeight;
  EGL_Texture * swSurfaceTexture;

  // scale algorithm
  int scaleAlgo;

  // night vision
  int nvMax;
  int nvGain;

  // colorblind mode
  int cbMode;

  bool useDMA;
  LG_RendererFormat format;

  // map HDR content to SDR
  bool  mapHDRtoSDR;
  bool  nativeHDR;
  bool  linearComposition;
  int   peakLuminance;
  int   maxCLL;

  EGL_PostProcess * pp;
  _Atomic(bool) processFrame;
};

// forwards
void toggleNV(int key, void * opaque);

static bool egl_initDesktopShader(
  struct DesktopShader * shader,
  const char * vertex_code  , size_t vertex_size,
  const char * fragment_code, size_t fragment_size,
  bool useDMA
)
{
  if (!egl_shaderInit(&shader->shader))
    return false;

  if (!egl_shaderCompile(shader->shader,
        vertex_code  , vertex_size,
        fragment_code, fragment_size,
        useDMA, NULL))
  {
    return false;
  }

  shader->uDesktopSize       =
    egl_shaderGetUniform(shader->shader, "desktopSize");
  shader->uTransform         =
    egl_shaderGetUniform(shader->shader, "transform");
  shader->uScaleAlgo         =
    egl_shaderGetUniform(shader->shader, "scaleAlgo");
  shader->uNVGain            =
    egl_shaderGetUniform(shader->shader, "nvGain");
  shader->uCBMode            =
    egl_shaderGetUniform(shader->shader, "cbMode");
  shader->uIsHDR             =
    egl_shaderGetUniform(shader->shader, "isHDR");
  shader->uMapHDRtoSDR       =
    egl_shaderGetUniform(shader->shader, "mapHDRtoSDR");
  shader->uMapHDRGain        =
    egl_shaderGetUniform(shader->shader, "mapHDRGain");
  shader->uMapHDRContentPeak =
    egl_shaderGetUniform(shader->shader, "mapHDRContentPeak");
  shader->uMapHDRPQ          =
    egl_shaderGetUniform(shader->shader, "mapHDRPQ");
  shader->uOutputHDRLinear   =
    egl_shaderGetUniform(shader->shader, "outputHDRLinear");

  return true;
}

bool egl_desktopInit(EGL * egl, EGL_Desktop ** desktop_, EGLDisplay * display,
    bool useDMA, int maxRects)
{
  EGL_Desktop * desktop = calloc(1, sizeof(EGL_Desktop));
  if (!desktop)
  {
    DEBUG_ERROR("Failed to malloc EGL_Desktop");
    return false;
  }
  *desktop_ = desktop;

  desktop->egl     = egl;
  desktop->display = display;

  if (!egl_textureInit(&desktop->texture, display,
        useDMA ? EGL_TEXTYPE_DMABUF : EGL_TEXTYPE_FRAMEBUFFER))
  {
    DEBUG_ERROR("Failed to initialize the desktop texture");
    return false;
  }

  if (!egl_desktopRectsInit(&desktop->mesh, maxRects))
  {
    DEBUG_ERROR("Failed to initialize the desktop mesh");
    return false;
  }

  if (!egl_initDesktopShader(
    &desktop->shader,
    b_shader_desktop_vert    , b_shader_desktop_vert_size,
    b_shader_desktop_rgb_frag, b_shader_desktop_rgb_frag_size,
    false))
  {
    DEBUG_ERROR("Failed to initialize the desktop shader");
    return false;
  }

  app_registerKeybind(KEY_N, toggleNV, desktop,
      "Toggle night vision mode");

  desktop->nvMax     = option_get_int("egl", "nvGainMax");
  desktop->nvGain    = option_get_int("egl", "nvGain"   );
  desktop->cbMode    = option_get_int("egl", "cbMode"   );
  desktop->scaleAlgo = option_get_int("egl", "scale"    );
  desktop->useDMA    = useDMA;

  desktop->mapHDRtoSDR   = option_get_bool("egl", "mapHDRtoSDR"  );
  desktop->peakLuminance = option_get_int ("egl", "peakLuminance");
  desktop->maxCLL        = option_get_int ("egl", "maxCLL"       );

  if (!egl_postProcessInit(&desktop->pp))
  {
    DEBUG_ERROR("Failed to initialize the post process manager");
    return false;
  }

  // this MUST be first
  egl_postProcessAdd(desktop->pp, &egl_filter24bitOps);

  egl_postProcessAdd(desktop->pp, &egl_filterDownscaleOps);
  egl_postProcessAdd(desktop->pp, &egl_filterFFXCASOps   );
  egl_postProcessAdd(desktop->pp, &egl_filterFFXFSR1Ops  );
  egl_postProcessAdd(desktop->pp, &egl_filterGLSLOps     );
  return true;
}

void toggleNV(int key, void * opaque)
{
  EGL_Desktop * desktop = (EGL_Desktop *)opaque;
  if (desktop->nvGain++ == desktop->nvMax)
    desktop->nvGain = 0;

       if (desktop->nvGain == 0) app_alert(LG_ALERT_INFO, "NV Disabled");
  else if (desktop->nvGain == 1) app_alert(LG_ALERT_INFO, "NV Enabled");
  else app_alert(LG_ALERT_INFO, "NV Gain + %d", desktop->nvGain - 1);

  app_invalidateWindow(true);
}

bool egl_desktopScaleValidate(struct Option * opt, const char ** error)
{
  if (opt->value.x_int >= 0 && opt->value.x_int < EGL_SCALE_MAX)
    return true;

  *error = "Invalid scale algorithm number";
  return false;
}

void egl_desktopFree(EGL_Desktop ** desktop)
{
  if (!*desktop)
    return;

  egl_textureFree    (&(*desktop)->texture         );
  egl_textureFree    (&(*desktop)->swSurfaceTexture);
  egl_shaderFree     (&(*desktop)->shader   .shader);
  egl_desktopRectsFree(&(*desktop)->mesh           );
  egl_postProcessFree(&(*desktop)->pp);

  free(*desktop);
  *desktop = NULL;
}

static const char * algorithmNames[EGL_SCALE_MAX] = {
  [EGL_SCALE_AUTO]    = "Automatic (downscale: linear, upscale: nearest)",
  [EGL_SCALE_NEAREST] = "Nearest",
  [EGL_SCALE_LINEAR]  = "Linear",
};

void egl_desktopConfigUI(EGL_Desktop * desktop)
{
  igText("Scale algorithm:");
  igPushItemWidth(igGetWindowWidth() - igGetStyle()->WindowPadding.x * 2);
  if (igBeginCombo("##scale", algorithmNames[desktop->scaleAlgo], 0))
  {
    for (int i = 0; i < EGL_SCALE_MAX; ++i)
    {
      bool selected = i == desktop->scaleAlgo;
      if (igSelectable_Bool(algorithmNames[i], selected, 0,
            (ImVec2) { 0.0f, 0.0f }))
        desktop->scaleAlgo = i;
      if (selected)
        igSetItemDefaultFocus();
    }
    igEndCombo();
  }
  igPopItemWidth();

  igText("Night vision mode:");
  igSameLine(0.0f, -1.0f);
  igPushItemWidth(igGetWindowWidth() - igGetCursorPosX() - igGetStyle()->WindowPadding.x);

  const char * format;
  switch (desktop->nvGain)
  {
    case 0: format = "off"; break;
    case 1: format = "on";  break;
    default: format = "gain: %d";
  }
  igSliderInt("##nvgain", &desktop->nvGain, 0, desktop->nvMax, format, 0);
  igPopItemWidth();

  bool mapHDRtoSDR   = desktop->mapHDRtoSDR;
  int  peakLuminance = desktop->peakLuminance;
  int  maxCLL        = desktop->maxCLL;

  igSeparator();
  igCheckbox("Map HDR content to SDR", &mapHDRtoSDR);
  igSliderInt("Peak Luminance", &peakLuminance, 1, 10000,
      "%d nits",
      ImGuiInputTextFlags_CharsDecimal);
  igSliderInt("Max content light level", &maxCLL, 1, 10000,
      "%d nits", ImGuiInputTextFlags_CharsDecimal);

  if (mapHDRtoSDR   != desktop->mapHDRtoSDR   ||
      peakLuminance != desktop->peakLuminance ||
      maxCLL        != desktop->maxCLL)
  {
    desktop->mapHDRtoSDR   = mapHDRtoSDR;
    desktop->peakLuminance = max(1, peakLuminance);
    desktop->maxCLL        = max(1, maxCLL);
    app_invalidateWindow(true);
  }
}

bool egl_desktopSetup(EGL_Desktop * desktop, const LG_RendererFormat format)
{
  memcpy(&desktop->format, &format, sizeof(LG_RendererFormat));

  enum EGL_PixelFormat pixFmt;
  switch(format.type)
  {
    case FRAME_TYPE_BGRA:
      pixFmt = EGL_PF_BGRA;
      break;

    case FRAME_TYPE_RGBA:
      pixFmt = EGL_PF_RGBA;
      break;

    case FRAME_TYPE_RGBA10:
      pixFmt = EGL_PF_RGBA10;
      break;

    case FRAME_TYPE_RGBA16F:
      pixFmt = EGL_PF_RGBA16F;
      break;

    case FRAME_TYPE_BGR_32:
      pixFmt = EGL_PF_BGR_32;
      break;

    case FRAME_TYPE_RGB_24:
      pixFmt = EGL_PF_RGB_24;
      break;

    default:
      DEBUG_ERROR("Unsupported frame format");
      return false;
  }

  desktop->width  = format.frameWidth;
  desktop->height = format.frameHeight;
  desktop->hdr    = format.hdr;
  desktop->hdrPQ  = format.hdrPQ;

  if (!egl_textureSetup(
    desktop->texture,
    pixFmt,
    desktop->format.dataWidth,
    desktop->format.dataHeight,
    desktop->format.stride,
    desktop->format.pitch
  ))
  {
    DEBUG_ERROR("Failed to setup the desktop texture");
    return false;
  }

  return true;
}

bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame,
    LG_RendererFrameToken frameToken, int dmaFd,
    const FrameDamageRect * damageRects, int damageRectsCount,
    uint64_t * waitTimeNs, LG_FrameReleaseFn releaseFn,
    void * releaseOpaque, uint64_t releaseHandle)
{
  /*
   * Both the DMA and staged upload paths need a complete frame. Do this
   * before selecting either path so a producer that is still filling an
   * indirect frame cannot be mistaken for a broken DMA import. Retain the
   * last image and release a direct frame lease when there is one.
   */
  if (unlikely(!framebuffer_wait_timed(
          frame, desktop->texture->format.dataSize, waitTimeNs)))
  {
    if (releaseFn)
      releaseFn(releaseOpaque, releaseHandle);
    return true;
  }

  if (likely(desktop->useDMA && dmaFd >= 0))
  {
    if (likely(egl_textureUpdateFromDMA(
            desktop->texture, frame, frameToken, dmaFd, waitTimeNs,
            releaseFn, releaseOpaque, releaseHandle)))
    {
      atomic_store(&desktop->processFrame, true);
      return true;
    }

    DEBUG_WARN("DMA update failed, disabling DMABUF imports");

    const char * vendor  = (const char *)glGetString(GL_VENDOR);
    if (strstr(vendor, "NVIDIA"))
    {
      DEBUG_WARN("NVIDIA's DMABUF support is incomplete, please direct your complaints to NVIDIA");
      DEBUG_WARN("This is not a bug in Looking Glass");
    }

    desktop->useDMA = false;

    const char * gl_exts = (const char *)glGetString(GL_EXTENSIONS);
    if (!util_hasGLExt(gl_exts, "GL_EXT_buffer_storage"))
    {
      DEBUG_ERROR("GL_EXT_buffer_storage is needed to use EGL backend");
      return false;
    }

    egl_textureFree(&desktop->texture);
    if (!egl_textureInit(&desktop->texture, desktop->display,
          EGL_TEXTYPE_FRAMEBUFFER))
    {
      DEBUG_ERROR("Failed to initialize the desktop texture");
      return false;
    }

    if (!egl_desktopSetup(desktop, desktop->format))
      return false;
  }

  /*
   * Streamed textures trigger post-processing when the render context submits
   * the upload. Signalling here can race with that submission and process the
   * previous texture contents instead.
   */
  if (likely(egl_textureUpdateFromFrame(desktop->texture, frame, frameToken,
        damageRects, damageRectsCount, waitTimeNs)))
  {
    if (releaseFn)
      releaseFn(releaseOpaque, releaseHandle);
    return true;
  }

  return false;
}

void egl_desktopRestart(EGL_Desktop * desktop)
{
  egl_textureReset(desktop->texture);
  atomic_store(&desktop->processFrame, false);
}

void egl_desktopPoll(EGL_Desktop * desktop)
{
  egl_texturePoll(desktop->texture);
}

void egl_desktopResize(EGL_Desktop * desktop, int width, int height)
{
  atomic_store(&desktop->processFrame, true);
}

bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
    unsigned int outputHeight, const float x, const float y,
    const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType,
    LG_RendererRotate rotate, const struct DamageRects * rects,
    LG_RendererFrameToken damageFrameToken,
    LG_RendererFrameToken frameTokenLimit, bool * fullFrame,
    LG_RendererFrameToken * consumedFrameToken,
    LG_RendererFrameToken * renderedFrameToken,
    uint64_t * effectsTime, EGL_Framebuffer * target)
{
  EGL_Texture * tex;
  int width, height;

  /* This also retires completed snapshots while the software surface is
   * shown. */
  egl_texturePoll(desktop->texture);

  if (unlikely(desktop->useSwSurface))
  {
    tex    = desktop->swSurfaceTexture;
    width  = desktop->swSurfaceWidth;
    height = desktop->swSurfaceHeight;
  }
  else
  {
    tex    = desktop->texture;
    width  = desktop->width;
    height = desktop->height;
  }

  if (unlikely(outputWidth == 0 || outputHeight == 0))
    DEBUG_FATAL("outputWidth || outputHeight == 0");

  LG_RendererFrameToken    frameToken;
  const enum EGL_TexStatus status =
    egl_textureProcess(tex, frameTokenLimit, &frameToken);
  const bool               textureUpdated = status == EGL_TEX_STATUS_UPDATED;

  *consumedFrameToken = frameToken;
  *renderedFrameToken = LG_RENDERER_FRAME_TOKEN_NONE;
  *effectsTime        = 0;
  if (unlikely(status != EGL_TEX_STATUS_OK && !textureUpdated))
  {
    if (status != EGL_TEX_STATUS_NOTREADY)
      DEBUG_ERROR("Failed to process the desktop texture");
  }

  *fullFrame = false;
  if (frameToken != LG_RENDERER_FRAME_TOKEN_NONE &&
      frameToken != damageFrameToken)
  {
    rects      = NULL;
    *fullFrame = true;
  }

  int scaleAlgo = EGL_SCALE_NEAREST;

  egl_desktopRectsMatrix(desktop->matrix,
      width, height, x, y, scaleX, scaleY, rotate);
  egl_desktopRectsUpdate(desktop->mesh,
      rects ? rects->rects : NULL, rects ? rects->count : -1,
      width, height);

  const bool hdr = desktop->hdr && !desktop->useSwSurface;
  uint32_t hdrPeak = 0;
  if (hdr)
  {
    hdrPeak = max(desktop->format.hdrMaxDisplayLuminance,
        desktop->format.hdrMaxContentLightLevel);
    if (!hdrPeak)
      hdrPeak = HDR_PEAK_LUMINANCE_DEFAULT;
    else
      hdrPeak = clamp(hdrPeak,
          HDR_PEAK_LUMINANCE_MIN, HDR_PEAK_LUMINANCE_MAX);
  }

  bool processFrame = textureUpdated;
  processFrame |= atomic_exchange(&desktop->processFrame, false);
  processFrame |= egl_postProcessConfigModified(desktop->pp);
  if (processFrame)
  {
    const uint64_t effectsStart  = nanotime();
    const bool     postProcessed = egl_postProcessRun(
        desktop->pp, tex, desktop->mesh,
        width, height, outputWidth, outputHeight, false,
        hdr && desktop->hdrPQ, (float)hdrPeak);
    *effectsTime = nanotime() - effectsStart;

    if (postProcessed && egl_postProcessNeedsFullFrame(desktop->pp))
    {
      /* The filter output may have changed everywhere, but this only applies
       * to the render that actually evaluated the filter. */
      egl_desktopRectsUpdate(desktop->mesh, NULL, -1, width, height);
      *fullFrame = true;
    }
  }

  unsigned int finalSizeX, finalSizeY;
  bool outputHDRPQ = desktop->hdrPQ;
  EGL_Texture * texture = egl_postProcessGetOutput(desktop->pp,
      &finalSizeX, &finalSizeY, &outputHDRPQ);

  if (unlikely(!texture))
  {
    texture     = tex;
    finalSizeX  = width;
    finalSizeY  = height;
    outputHDRPQ = desktop->hdrPQ;
  }

  if (target)
    egl_framebufferBind(target);
  else
  {
    egl_stateBindFramebuffer(0);
    egl_resetViewport(desktop->egl);
  }

  const EGL_TexStatus bindStatus = egl_textureBind(texture);
  if (unlikely(bindStatus != EGL_TEX_STATUS_OK))
  {
    if (bindStatus != EGL_TEX_STATUS_NOTREADY)
      DEBUG_ERROR("Failed to bind the desktop texture");
    if (!desktop->useSwSurface && desktop->useDMA && processFrame)
      egl_textureMarkUsed(desktop->texture);
    return false;
  }
  *renderedFrameToken = tex->frameToken;

  if (finalSizeX > width || finalSizeY > height)
    scaleType = EGL_DESKTOP_DOWNSCALE;

  switch (desktop->scaleAlgo)
  {
    case EGL_SCALE_AUTO:
      switch (scaleType)
      {
        case EGL_DESKTOP_UPSCALE:
          scaleAlgo = EGL_SCALE_NEAREST;
          break;

        case EGL_DESKTOP_NOSCALE:
        case EGL_DESKTOP_DOWNSCALE:
          scaleAlgo = EGL_SCALE_LINEAR;
          break;
      }
      break;

    default:
      scaleAlgo = desktop->scaleAlgo;
  }

  const struct DesktopShader * shader = &desktop->shader;

  /* PQ is normalized to 10000 nits, while scRGB defines 1.0 as 80 nits.
   * Convert either encoding into values relative to the selected SDR peak. */
  const float mapHDRGain        = (outputHDRPQ ? 10000.0f : 80.0f) /
    desktop->peakLuminance;
  const float mapHDRContentPeak =
    (float)desktop->maxCLL / desktop->peakLuminance;

  egl_uniform1i         (shader->uScaleAlgo        , scaleAlgo);
  egl_uniform2f         (shader->uDesktopSize      , width, height);
  egl_uniformMatrix3x2fv(shader->uTransform        ,
      1, GL_FALSE, desktop->matrix);
  egl_uniform1f         (shader->uNVGain           , desktop->nvGain);
  egl_uniform1i         (shader->uCBMode           , desktop->cbMode);
  egl_uniform1i         (shader->uIsHDR            , hdr);
  egl_uniform1i         (shader->uMapHDRtoSDR      ,
      desktop->mapHDRtoSDR && !desktop->nativeHDR);
  egl_uniform1f         (shader->uMapHDRGain       , mapHDRGain);
  egl_uniform1f         (shader->uMapHDRContentPeak, mapHDRContentPeak);
  egl_uniform1i         (shader->uMapHDRPQ         , outputHDRPQ);
  egl_uniform1i         (shader->uOutputHDRLinear  ,
      desktop->linearComposition);
  egl_shaderUse(shader->shader);
  egl_desktopRectsRender(desktop->mesh);
  if (!desktop->useSwSurface && desktop->useDMA &&
      (processFrame || texture == tex))
    egl_textureMarkUsed(desktop->texture);
  return true;
}

void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR,
    bool linearComposition)
{
  desktop->nativeHDR         = nativeHDR;
  desktop->linearComposition = nativeHDR && linearComposition;
}

void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled,
    float * gain, float * contentPeak)
{
  *enabled     = desktop->hdr && !desktop->useSwSurface &&
    desktop->mapHDRtoSDR && !desktop->nativeHDR;
  *gain        = (desktop->hdrPQ ? 10000.0f : 80.0f) /
    desktop->peakLuminance;
  *contentPeak = (float)desktop->maxCLL / desktop->peakLuminance;
}

void egl_desktopSwSurfaceConfigure(EGL_Desktop * desktop,
    int width, int height)
{
  if (!desktop->swSurfaceTexture)
    if (!egl_textureInit(&desktop->swSurfaceTexture, desktop->display,
          EGL_TEXTYPE_BUFFER_MAP))
    {
      DEBUG_ERROR("Failed to initialize the software surface texture");
      return;
    }

  if (!egl_textureSetup(
    desktop->swSurfaceTexture,
    EGL_PF_BGRA,
    width,
    height,
    width,
    width * 4
  ))
  {
    DEBUG_ERROR("Failed to setup the software surface texture");
    return;
  }

  desktop->swSurfaceWidth  = width;
  desktop->swSurfaceHeight = height;
}

void egl_desktopSwSurfaceDrawFill(EGL_Desktop * desktop,
    int x, int y, int width, int height, uint32_t color)
{
  if (!desktop->swSurfaceTexture || width <= 0 || height <= 0)
    return;

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

  x      = x < 0 ? 0 : x;
  y      = y < 0 ? 0 : y;
  width  = (right  > desktop->swSurfaceWidth  ?
      desktop->swSurfaceWidth  : (int)right ) - x;
  height = (bottom > desktop->swSurfaceHeight ?
      desktop->swSurfaceHeight : (int)bottom) - y;

  if (egl_texBufferStreamFill(
        desktop->swSurfaceTexture, x, y, width, height, color))
    atomic_store(&desktop->processFrame, true);
}

void egl_desktopSwSurfaceDrawBitmap(EGL_Desktop * desktop,
    int x, int y, int width, int height, int stride, uint8_t * data,
    bool topDown)
{
  if (!desktop->swSurfaceTexture || !data ||
      width <= 0 || height <= 0 || stride <= 0 || width > stride / 4)
    return;

  const int originalHeight = height;
  const int64_t right      = (int64_t)x + width;
  const int64_t bottom     = (int64_t)y + height;
  if (x >= desktop->swSurfaceWidth ||
      y >= desktop->swSurfaceHeight ||
      right <= 0 || bottom <= 0)
    return;

  const int sourceX = x < 0 ? -x : 0;
  const int sourceY = y < 0 ? -y : 0;
  x      = x < 0 ? 0 : x;
  y      = y < 0 ? 0 : y;
  width  = (right  > desktop->swSurfaceWidth  ?
      desktop->swSurfaceWidth  : (int)right ) - x;
  height = (bottom > desktop->swSurfaceHeight ?
      desktop->swSurfaceHeight : (int)bottom) - y;

  const int sourceRow = topDown ?
    sourceY : originalHeight - sourceY - height;
  data += (size_t)sourceRow * stride + (size_t)sourceX * 4;

  egl_textureUpdateRect(desktop->swSurfaceTexture,
      x, y, width, height, stride / 4, stride, data, topDown);
  atomic_store(&desktop->processFrame, true);
}

void egl_desktopSwSurfaceShow(EGL_Desktop * desktop, bool show)
{
  desktop->useSwSurface = show;
  atomic_store(&desktop->processFrame, true);
}
