/**
 * 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
 */

#define _GNU_SOURCE //needed for pthread_setname_np

#include <obs/obs-config.h>
#include <obs/obs-module.h>
#include <obs/util/threading.h>
#include <obs/util/platform.h>
#include <obs/graphics/graphics.h>
#include <obs/graphics/matrix4.h>

#include <common/array.h>
#include <common/ivshmem.h>
#include <common/KVMFR.h>
#include <common/LGMPConfig.h>
#include <common/framebuffer.h>
#include <lgmp/client.h>

#include <stdio.h>
#include <unistd.h>
#include <stdatomic.h>

#include "rgb24.effect.h"
#include "hdrpq.effect.h"
#include "cursor.effect.h"
#include "frame_scheduler.h"

#define LG_FRAME_TIMING_SPIN_COUNT 4096

/* scRGB reference white in cd/m² (OBS GS_CS_709_SCRGB is defined as
 * 1.0 = 80 cd/m²). PQ encodes an absolute 0..10000 cd/m² range, so linear
 * PQ output is scaled by 10000 / 80 to reach the scRGB convention. */
#define LG_SCRGB_REFERENCE_WHITE 80.0f

/**
 * the following comes from drm_fourcc.h and is included here to avoid the
 * external dependency for the few simple defines we need
 */
#define fourcc_code(a, b, c, d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
         ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
#define DRM_FORMAT_ARGB8888      fourcc_code('A', 'R', '2', '4')
#define DRM_FORMAT_XRGB8888      fourcc_code('X', 'R', '2', '4')
#define DRM_FORMAT_XBGR8888      fourcc_code('X', 'B', '2', '4')
#define DRM_FORMAT_XBGR2101010   fourcc_code('X', 'B', '3', '0')
#define DRM_FORMAT_XBGR16161616F fourcc_code('X', 'B', '4', 'H')

typedef enum
{
  STATE_STOPPED,
  STATE_OPEN,
  STATE_STARTING,
  STATE_RUNNING,
  STATE_STOPPING,
  STATE_RESTARTING
}
LGState;

#if LIBOBS_API_MAJOR_VER >= 27
typedef struct
{
  const KVMFRFrame * frame;
  size_t             dataSize;
  int                fd;
  gs_texture_t     * texture;
}
DMAFrameInfo;
#endif

typedef struct
{
  obs_source_t       * context;
  LGState              state;
  char               * shmFile;
  uint32_t             formatVer;
  uint32_t             screenWidth, screenHeight;
  uint32_t             dataWidth, dataHeight;
  uint32_t             frameWidth, frameHeight;
  enum gs_color_format format;
  bool                 unpack;
  uint32_t             drmFormat;
  struct vec2          screenScale;
  FrameType            type;
  int                  bpp;
  struct IVSHMEM       shmDev;
  PLGMPClient          lgmp;
  PLGMPClientQueue     frameQueue;
  PLGMPClientQueue     frameOwnerQueue[LGMP_Q_FRAME_LEN];
  PLGMPClientQueue     pointerQueue;
  gs_texture_t       * texture;
  gs_texture_t       * dstTexture;
  bool                 textureMapped;
  uint8_t            * texData;
  uint32_t             linesize;

  bool                 hideMouse;
  bool                 hdr;
  bool                 hdrPQ;
  _Atomic(float)       sdrWhiteLevel;  // source cursor white level in nits
  struct vec3          colorMatrix[3]; // source primaries -> BT.709 (linear)
  float                hdrScale;       // scRGB reference white scaling
#if LIBOBS_API_MAJOR_VER >= 27
  bool                 dmabuf;
  bool                 dmabufTested;
  DMAFrameInfo         dmaInfo[LGMP_Q_FRAME_BUFFER_LEN];
  gs_texrender_t     * dmaCopyRender;
  gs_samplerstate_t  * dmaCopySampler;
  gs_timer_t         * dmaCopyTimer;
#endif

#if LIBOBS_API_MAJOR_VER >= 28
  enum gs_color_space colorSpace;
#endif

  pthread_t            frameThread, pointerThread;
  os_sem_t           * frameSem;
  uint32_t             frameSerial;
  bool                 frameSerialValid;
  pthread_mutex_t      pointerLock;
  LGFrameScheduler     frameScheduler;
  _Atomic(bool)        sourceActive;

  bool                 cursorMono;
  gs_texture_t       * cursorTex;
  gs_texture_t       * cursorXorTex;
  struct gs_rect       cursorRect;

  bool                 cursorVisible;
  KVMFRCursor          cursor;
  os_sem_t           * cursorSem;
  atomic_uint          cursorVer;
  unsigned int         cursorCurVer;
  KVMFRColorTransform  pendingCursorTransform;
  KVMFRColorTransform  activeCursorTransform;
  atomic_uint          cursorTransformVer;
  unsigned int         cursorTransformCurVer;
  gs_texture_t       * cursorLUT;
  uint32_t             cursorSize;
  uint32_t           * cursorData;

  gs_effect_t * unpackEffect;
  gs_eparam_t * image;
  gs_eparam_t * outputSize;
  gs_eparam_t * swap;

  gs_effect_t * hdrEffect;
  gs_eparam_t * hdrImage;
  gs_eparam_t * hdrOutputSize;
  gs_eparam_t * hdrColorMatrix[3];
  gs_eparam_t * hdrScaleParam;

  gs_effect_t * cursorEffect;
  gs_eparam_t * cursorImage;
  gs_eparam_t * cursorMultiplier;
  gs_eparam_t * cursorSDRWhiteLevel;
  gs_eparam_t * cursorOutputTransfer;
  gs_eparam_t * cursorMatrixEnabled;
  gs_eparam_t * cursorLUTEnabled;
  gs_eparam_t * cursorColorMatrix[3];
  gs_eparam_t * cursorColorScalar;
  gs_eparam_t * cursorColorLUT;
}
LGPlugin;

typedef struct
{
  LGMPMessage      msg;
  PLGMPClientQueue queue;
  uint32_t         scheduleEpoch;
  uint32_t         scheduleDeadlineSerial;
  bool             owner;
}
LGFrameMessage;

static void * frameThread(void * data);
static void * pointerThread(void * data);
static void lgUpdate(void * data, obs_data_t * settings);

static uint64_t lgFramePeriod(void)
{
  struct obs_video_info videoInfo;
  if (!obs_get_video_info(&videoInfo) ||
      !videoInfo.fps_num || !videoInfo.fps_den)
    return 0;

  return 1000000000ULL * videoInfo.fps_den / videoInfo.fps_num;
}

static const char * lgGetName(void * unused)
{
  return obs_module_text("Looking Glass Client");
}

static bool lgFrameSerialNewer(uint32_t lhs, uint32_t rhs)
{
  return (int32_t)(lhs - rhs) > 0;
}

static bool lgFrameMessageHasMatchingDeadline(
    bool owner, const LGMPMessage * message, const KVMFRFrame * frame)
{
  if (!owner)
    return false;

  const uint32_t epoch = (uint32_t)(message->udata >> 32);
  const uint32_t deadlineSerial = (uint32_t)message->udata;
  return deadlineSerial && epoch == frame->scheduleEpoch &&
    deadlineSerial == frame->scheduleDeadlineSerial;
}

static LGMP_STATUS lgFrameProcessNewest(LGPlugin * this,
    LGFrameMessage * result)
{
  struct
  {
    PLGMPClientQueue queue;
    bool             owner;
  }
  queues[LGMP_Q_FRAME_LEN + 1] =
  {
    { this->frameQueue, false },
  };
  for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
  {
    queues[i + 1].queue = this->frameOwnerQueue[i];
    queues[i + 1].owner = true;
  }

  memset(result, 0, sizeof(*result));
  for (unsigned int i = 0; i < ARRAY_LENGTH(queues); ++i)
  {
    if (!queues[i].queue)
      continue;

    LGMP_STATUS status = lgmpClientAdvanceToLast(queues[i].queue);
    if (status != LGMP_OK && status != LGMP_ERR_QUEUE_EMPTY)
      return status;

    LGMPMessage msg;
    status = lgmpClientProcess(queues[i].queue, &msg);
    if (status == LGMP_ERR_QUEUE_EMPTY)
      continue;
    if (status != LGMP_OK)
      return status;

    const KVMFRFrame * frame = (const KVMFRFrame *)msg.mem;
    const bool owner = queues[i].owner || msg.udata != 0;

    if (!result->queue)
    {
      result->msg   = msg;
      result->queue = queues[i].queue;
      result->owner = owner;
      continue;
    }

    const KVMFRFrame * selected = (const KVMFRFrame *)result->msg.mem;
    const bool replace = lgFrameSerialNewer(
      frame->frameSerial, selected->frameSerial) ||
      (frame->frameSerial == selected->frameSerial &&
        ((owner && !result->owner) ||
         (lgFrameMessageHasMatchingDeadline(owner, &msg, frame) &&
          !lgFrameMessageHasMatchingDeadline(
            result->owner, &result->msg, selected))));
    PLGMPClientQueue discard = queues[i].queue;
    if (replace)
    {
      discard       = result->queue;
      result->msg   = msg;
      result->queue = queues[i].queue;
      result->owner = owner;
    }

    status = lgmpClientMessageDone(discard);
    if (status != LGMP_OK)
      return status;
  }

  if (result->owner)
  {
    result->scheduleEpoch          = (uint32_t)(result->msg.udata >> 32);
    result->scheduleDeadlineSerial = (uint32_t)result->msg.udata;
  }

  return result->queue ? LGMP_OK : LGMP_ERR_QUEUE_EMPTY;
}

static bool lgFrameTimingReady(const KVMFRFrame * frame)
{
  return __atomic_load_n(&frame->timingValid, __ATOMIC_ACQUIRE) &&
    frame->timingSerial == frame->frameSerial;
}

static bool lgFramePhaseIdentity(const LGFrameMessage * message,
    const KVMFRFrame * frame, uint32_t * generation)
{
  if (!message->owner || !message->scheduleEpoch ||
      !message->scheduleDeadlineSerial)
    return false;

  for (unsigned i = 0;
       !lgFrameTimingReady(frame) && i < LG_FRAME_TIMING_SPIN_COUNT;
       ++i)
  {
  }

  if (!lgFrameTimingReady(frame) ||
      !(frame->timingFlags & KVMFR_FRAME_TIMING_PHASE_VALID) ||
      !frame->scheduleGeneration ||
      frame->scheduleEpoch != message->scheduleEpoch ||
      frame->scheduleDeadlineSerial != message->scheduleDeadlineSerial)
    return false;

  *generation = frame->scheduleGeneration;
  return true;
}

static void lgFrameUnsubscribeOwnerQueues(LGPlugin * this)
{
  for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
  {
    if (this->frameOwnerQueue[i])
      lgmpClientUnsubscribe(&this->frameOwnerQueue[i]);
    this->frameOwnerQueue[i] = NULL;
  }
}

static void * lgCreate(obs_data_t * settings, obs_source_t * context)
{
  LGPlugin * this = bzalloc(sizeof(LGPlugin));

  this->context = context;
#if LIBOBS_API_MAJOR_VER >= 27
  for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
    this->dmaInfo[i].fd = -1;
#endif

  obs_enter_graphics();
  char * error = NULL;
  this->unpackEffect = gs_effect_create(b_effect_rgb24_effect, "rbg24", &error);
  if (!this->unpackEffect)
  {
    blog(LOG_ERROR, "%s", error);
    bfree(error);
    bfree(this);
    obs_leave_graphics();
    return NULL;
  }

  this->image      = gs_effect_get_param_by_name(
      this->unpackEffect, "image"     );
  this->outputSize = gs_effect_get_param_by_name(
      this->unpackEffect, "outputSize");
  this->swap       = gs_effect_get_param_by_name(
      this->unpackEffect, "swap"      );

  this->hdrEffect = gs_effect_create(b_effect_hdrpq_effect, "hdrpq", &error);
  if (!this->hdrEffect)
  {
    blog(LOG_ERROR, "%s", error);
    bfree(error);
    gs_effect_destroy(this->unpackEffect);
    bfree(this);
    obs_leave_graphics();
    return NULL;
  }

  this->hdrImage         = gs_effect_get_param_by_name(
      this->hdrEffect, "image"       );
  this->hdrOutputSize    = gs_effect_get_param_by_name(
      this->hdrEffect, "outputSize"  );
  this->hdrColorMatrix[0] = gs_effect_get_param_by_name(
      this->hdrEffect, "colorMatrix0");
  this->hdrColorMatrix[1] = gs_effect_get_param_by_name(
      this->hdrEffect, "colorMatrix1");
  this->hdrColorMatrix[2] = gs_effect_get_param_by_name(
      this->hdrEffect, "colorMatrix2");
  this->hdrScaleParam    = gs_effect_get_param_by_name(
      this->hdrEffect, "scale"       );

  this->cursorEffect = gs_effect_create(
      b_effect_cursor_effect, "cursor", &error);
  if (!this->cursorEffect)
  {
    blog(LOG_ERROR, "%s", error);
    bfree(error);
    gs_effect_destroy(this->hdrEffect);
    gs_effect_destroy(this->unpackEffect);
    bfree(this);
    obs_leave_graphics();
    return NULL;
  }

  this->cursorImage      = gs_effect_get_param_by_name(
      this->cursorEffect, "image"     );
  this->cursorMultiplier = gs_effect_get_param_by_name(
      this->cursorEffect, "multiplier");
  this->cursorSDRWhiteLevel = gs_effect_get_param_by_name(
      this->cursorEffect, "sdrWhiteLevel");
  this->cursorOutputTransfer = gs_effect_get_param_by_name(
      this->cursorEffect, "outputTransfer");
  this->cursorMatrixEnabled = gs_effect_get_param_by_name(
      this->cursorEffect, "matrixEnabled");
  this->cursorLUTEnabled = gs_effect_get_param_by_name(
      this->cursorEffect, "lutEnabled");
  this->cursorColorMatrix[0] = gs_effect_get_param_by_name(
      this->cursorEffect, "colorMatrix0");
  this->cursorColorMatrix[1] = gs_effect_get_param_by_name(
      this->cursorEffect, "colorMatrix1");
  this->cursorColorMatrix[2] = gs_effect_get_param_by_name(
      this->cursorEffect, "colorMatrix2");
  this->cursorColorScalar = gs_effect_get_param_by_name(
      this->cursorEffect, "colorScalar");
  this->cursorColorLUT = gs_effect_get_param_by_name(
      this->cursorEffect, "colorLUT");
  obs_leave_graphics();

  os_sem_init (&this->frameSem , 0);
  os_sem_init (&this->cursorSem, 1);
  pthread_mutex_init(&this->pointerLock, NULL);
  atomic_store(&this->cursorVer, 0);
  atomic_store(&this->cursorTransformVer, 0);
  atomic_store(&this->sdrWhiteLevel, KVMFR_SDR_WHITE_LEVEL_DEFAULT);
  atomic_store(&this->sourceActive, obs_source_active(context));
  lgUpdate(this, settings);
  return this;
}

static void createThreads(LGPlugin * this)
{
  pthread_create(&this->frameThread, NULL, frameThread, this);
  pthread_setname_np(this->frameThread, "LGFrameThread");
  pthread_create(&this->pointerThread, NULL, pointerThread, this);
  pthread_setname_np(this->pointerThread, "LGPointerThread");
}

static void waitThreads(LGPlugin * this)
{
  pthread_join(this->frameThread,   NULL);
  pthread_join(this->pointerThread, NULL);
}

#if LIBOBS_API_MAJOR_VER >= 27
/* The caller must hold the OBS graphics lock. */
static void dmabufInvalidateTextures(LGPlugin * this)
{
  for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
  {
    if (!this->dmaInfo[i].texture)
      continue;

    gs_texture_destroy(this->dmaInfo[i].texture);
    this->dmaInfo[i].texture = NULL;
  }

  this->dmabufTested = false;
}

/* The caller must hold the OBS graphics lock. */
static void dmabufReset(LGPlugin * this)
{
  dmabufInvalidateTextures(this);

  if (this->dmaCopyRender)
  {
    gs_texrender_destroy(this->dmaCopyRender);
    this->dmaCopyRender = NULL;
  }

  if (this->dmaCopySampler)
  {
    gs_samplerstate_destroy(this->dmaCopySampler);
    this->dmaCopySampler = NULL;
  }

  if (this->dmaCopyTimer)
  {
    gs_timer_destroy(this->dmaCopyTimer);
    this->dmaCopyTimer = NULL;
  }

  /* Ensure the driver releases every DMA-BUF attachment before the exported
   * FDs and their backing IVSHMEM device are closed. */
  gs_flush();

  for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
  {
    DMAFrameInfo * fi = &this->dmaInfo[i];
    if (fi->fd >= 0)
      close(fi->fd);

    memset(fi, 0, sizeof(*fi));
    fi->fd = -1;
  }
}
#endif

static void deinit(LGPlugin * this)
{
  bool closeShm = false;
  switch(this->state)
  {
    case STATE_STARTING:
      /* wait for startup to finish */
      while(this->state == STATE_STARTING)
        usleep(1);
      /* fallthrough */

    case STATE_RUNNING:
    case STATE_STOPPING:
    case STATE_RESTARTING:
      this->state = STATE_STOPPING;
      waitThreads(this);
      this->state = STATE_STOPPED;
      /* fallthrough */

    case STATE_OPEN:
      closeShm = true;
      break;

    case STATE_STOPPED:
      break;
  }

  if (this->shmFile)
  {
    bfree(this->shmFile);
    this->shmFile = NULL;
  }

  obs_enter_graphics();
  if (this->unpack && this->dstTexture)
  {
    gs_texture_destroy(this->dstTexture);
    this->dstTexture = NULL;
    this->unpack     = false;
  }

  if (this->texture)
  {
    if (this->textureMapped)
      gs_texture_unmap(this->texture);
    gs_texture_destroy(this->texture);
    this->texture       = NULL;
    this->textureMapped = false;
  }
  this->texData  = NULL;
  this->linesize = 0;

  if (this->cursorTex)
  {
    gs_texture_destroy(this->cursorTex);
    this->cursorTex = NULL;
  }

  if (this->cursorXorTex)
  {
    gs_texture_destroy(this->cursorXorTex);
    this->cursorXorTex = NULL;
  }

  if (this->cursorLUT)
  {
    gs_effect_set_texture(this->cursorColorLUT, NULL);
    gs_texture_destroy(this->cursorLUT);
    this->cursorLUT = NULL;
  }
  memset(&this->pendingCursorTransform, 0,
      sizeof(this->pendingCursorTransform));
  memset(&this->activeCursorTransform, 0,
      sizeof(this->activeCursorTransform));
  this->pendingCursorTransform.scalar = 1.0f;
  this->activeCursorTransform.scalar = 1.0f;
  atomic_store(&this->cursorTransformVer, 0);
  this->cursorTransformCurVer = 0;

#if LIBOBS_API_MAJOR_VER >= 27
  dmabufReset(this);
#endif

  obs_leave_graphics();

  if (closeShm)
  {
    lgmpClientFree(&this->lgmp);
    ivshmemClose(&this->shmDev);
  }

  this->state = STATE_STOPPED;
}

static void lgDestroy(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  deinit(this);
  os_sem_destroy(this->frameSem );
  os_sem_destroy(this->cursorSem);
  pthread_mutex_destroy(&this->pointerLock);

  obs_enter_graphics();
  gs_effect_destroy(this->unpackEffect);
  gs_effect_destroy(this->hdrEffect);
  gs_effect_destroy(this->cursorEffect);
  obs_leave_graphics();

  bfree(this);
}

static void lgGetDefaults(obs_data_t * defaults)
{
  obs_data_set_default_string(defaults, "shmFile", "/dev/kvmfr0");
#if LIBOBS_API_MAJOR_VER >= 27
  obs_data_set_default_bool(defaults, "dmabuf", true);
#endif
}

static obs_properties_t * lgGetProperties(void * data)
{
  obs_properties_t * props = obs_properties_create();

  obs_properties_add_text(props, "shmFile",
      obs_module_text("SHM File"), OBS_TEXT_DEFAULT);
  obs_properties_add_bool(props, "hideMouse",
      obs_module_text("Hide mouse cursor"));
#if LIBOBS_API_MAJOR_VER >= 27
  obs_properties_add_bool(props, "dmabuf",
      obs_module_text("Use DMABUF import (requires kvmfr device)"));
#else
  obs_property_t * dmabuf = obs_properties_add_bool(props, "dmabuf",
      obs_module_text("Use DMABUF import (requires OBS 27+ and kvmfr device)"));
  obs_property_set_enabled(dmabuf, false);
#endif

  return props;
}

static void * frameThread(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    this->frameOwnerQueue[i] = NULL;

  this->frameQueue       = NULL;
  this->frameSerial      = 0;
  this->frameSerialValid = false;

  if (lgmpClientSubscribe(
        this->lgmp, LGMP_Q_FRAME, &this->frameQueue) != LGMP_OK)
  {
    this->state = STATE_STOPPING;
    return NULL;
  }

  if (this->frameScheduler.supported)
  {
    for (unsigned int i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    {
      if (lgmpClientSubscribe(
            this->lgmp, LGMP_Q_FRAME_OWNER + i,
            &this->frameOwnerQueue[i]) == LGMP_OK)
        continue;

      lgFrameUnsubscribeOwnerQueues(this);
      lgmpClientUnsubscribe(&this->frameQueue);
      this->frameQueue = NULL;
      this->state      = STATE_STOPPING;
      return NULL;
    }
  }

  lgFrameSchedulerRequestImmediate(&this->frameScheduler);

  this->state = STATE_RUNNING;
  os_sem_post(this->frameSem);

  while(this->state == STATE_RUNNING)
  {
    os_sem_wait(this->frameSem);
    LGFrameMessage frameMessage;
    const LGMP_STATUS status = lgFrameProcessNewest(this, &frameMessage);
    if (status != LGMP_OK && status != LGMP_ERR_QUEUE_EMPTY)
    {
      os_sem_post(this->frameSem);
      printf("lgFrameProcessNewest: %s\n", lgmpStatusString(status));
      break;
    }

    uint64_t now = os_gettime_ns();
    if (status == LGMP_OK)
    {
      const KVMFRFrame * frame =
        (const KVMFRFrame *)frameMessage.msg.mem;
      if (frameMessage.owner)
      {
        const FrameBuffer * fb =
          (const FrameBuffer *)((const uint8_t *)frame + frame->offset);
        if (framebuffer_wait(
              fb, (size_t)frame->dataHeight * frame->pitch))
        {
          now = os_gettime_ns();
          uint32_t generation;
          if (lgFramePhaseIdentity(&frameMessage, frame, &generation))
            lgFrameSchedulerObserveFrame(&this->frameScheduler,
              frame->frameSerial, generation, frameMessage.scheduleEpoch,
              frameMessage.scheduleDeadlineSerial, now);
        }
      }
    }

    pthread_mutex_lock(&this->pointerLock);
    lgFrameSchedulerSetActive(&this->frameScheduler,
      atomic_load_explicit(&this->sourceActive, memory_order_acquire));
    lgFrameSchedulerUpdate(&this->frameScheduler,
      this->pointerQueue, now);
    pthread_mutex_unlock(&this->pointerLock);
    os_sem_post(this->frameSem);
    usleep(1000);
  }

  lgFrameUnsubscribeOwnerQueues(this);
  lgmpClientUnsubscribe(&this->frameQueue);
  this->frameQueue = NULL;
  this->state      = STATE_RESTARTING;
  return NULL;
}

inline static void allocCursorData(LGPlugin * this, const unsigned int size)
{
  if (this->cursorSize >= size)
    return;

  bfree(this->cursorData);
  this->cursorSize = size;
  this->cursorData = bmalloc(size);
}

static void * pointerThread(void * data)
{
  LGPlugin * this = (LGPlugin *)data;

  pthread_mutex_lock(&this->pointerLock);
  const LGMP_STATUS subscribeStatus = lgmpClientSubscribe(
    this->lgmp, LGMP_Q_POINTER, &this->pointerQueue);
  pthread_mutex_unlock(&this->pointerLock);
  if (subscribeStatus != LGMP_OK)
  {
    this->state = STATE_STOPPING;
    return NULL;
  }

  while(this->state == STATE_RUNNING)
  {
    LGMP_STATUS status;
    LGMPMessage msg;

    pthread_mutex_lock(&this->pointerLock);
    if ((status = lgmpClientProcess(this->pointerQueue, &msg)) != LGMP_OK)
    {
      pthread_mutex_unlock(&this->pointerLock);
      if (status != LGMP_ERR_QUEUE_EMPTY)
      {
        printf("lgmpClientProcess: %s\n", lgmpStatusString(status));
        break;
      }

      usleep(1000);
      continue;
    }

    const KVMFRCursor * const cursor = (const KVMFRCursor * const)msg.mem;
    if (msg.udata & CURSOR_FLAG_VISIBLE_VALID)
    {
      this->cursorVisible = this->hideMouse ?
        false : (msg.udata & CURSOR_FLAG_VISIBLE) != 0;
      if (cursor->sdrWhiteLevel)
        atomic_store(&this->sdrWhiteLevel, cursor->sdrWhiteLevel);
    }

    if (msg.udata & CURSOR_FLAG_SHAPE)
    {
      os_sem_wait(this->cursorSem);
      const uint8_t * const data = (const uint8_t * const)(cursor + 1);
      unsigned int dataSize = 0;

      switch(cursor->type)
      {
        case CURSOR_TYPE_MASKED_COLOR:
        {
          const size_t pixels = (size_t)cursor->width * cursor->height;
          dataSize = pixels * sizeof(uint32_t) * 2;
          allocCursorData(this, dataSize);

          uint32_t * normal = this->cursorData;
          uint32_t * xorData = normal + pixels;
          for(int y = 0; y < cursor->height; ++y)
            for(int x = 0; x < cursor->width; ++x)
            {
              const uint32_t src = *(const uint32_t *)
                (data + cursor->pitch * y + sizeof(uint32_t) * x);

              const uint32_t rgb    = src & 0x00FFFFFF;
              const bool     masked = src & 0xFF000000;
              const size_t   pos    = y * cursor->width + x;

              if (masked)
              {
                normal [pos] = 0x00000000;
                xorData[pos] = rgb ? rgb | 0xFF000000 : 0x00000000;
              }
              else
              {
                normal [pos] = rgb | 0xFF000000;
                xorData[pos] = 0x00000000;
              }
            }
          break;
        }

        case CURSOR_TYPE_COLOR:
        {
          dataSize = cursor->height * cursor->pitch;
          allocCursorData(this, dataSize);
          memcpy(this->cursorData, data, dataSize);
          break;
        }

        case CURSOR_TYPE_MONOCHROME:
        {
          const int height = cursor->height / 2;
          dataSize = height * cursor->width * sizeof(uint32_t);
          allocCursorData(this, dataSize);

          uint32_t * d = this->cursorData;
          for(int y = 0; y < height; ++y)
            for(int x = 0; x < cursor->width; ++x)
            {
              const uint8_t  * srcAnd  = data   + (cursor->pitch * y) + (x / 8);
              const uint8_t  * srcXor  = srcAnd + cursor->pitch * height;
              const uint8_t    mask    = 0x80 >> (x % 8);
              const bool       andMask = *srcAnd & mask;
              const bool       xorMask = *srcXor & mask;
              d[y * cursor->width + x] = andMask ?
                (xorMask ? 0xFFFFFFFF : 0x00000000) :
                (xorMask ? 0x00FFFFFF : 0xFF000000);
            }

          break;
        }

        default:
          printf("Invalid cursor type\n");
          break;
      }

      this->cursor.type   = cursor->type;
      this->cursor.width  = cursor->width;
      this->cursor.height = cursor->type == CURSOR_TYPE_MONOCHROME ?
        cursor->height / 2 : cursor->height;

      atomic_fetch_add_explicit(&this->cursorVer, 1, memory_order_relaxed);
      os_sem_post(this->cursorSem);
    }

    if (msg.udata & CURSOR_FLAG_COLOR_TRANSFORM)
    {
      const size_t shapeSize = msg.udata & CURSOR_FLAG_SHAPE ?
        (size_t)cursor->height * cursor->pitch : 0;
      const KVMFRColorTransform * transform =
        (const KVMFRColorTransform *)((const uint8_t *)(cursor + 1) +
          shapeSize);

      os_sem_wait(this->cursorSem);
      this->pendingCursorTransform = *transform;
      atomic_fetch_add_explicit(
          &this->cursorTransformVer, 1, memory_order_release);
      os_sem_post(this->cursorSem);
    }

    if (msg.udata & CURSOR_FLAG_POSITION)
    {
      this->cursor.x = cursor->x;
      this->cursor.y = cursor->y;
    }

    lgmpClientMessageDone(this->pointerQueue);
    pthread_mutex_unlock(&this->pointerLock);
  }

  pthread_mutex_lock(&this->pointerLock);
  lgmpClientUnsubscribe(&this->pointerQueue);
  pthread_mutex_unlock(&this->pointerLock);

  bfree(this->cursorData);
  this->cursorData = NULL;
  this->cursorSize = 0;

  this->state = STATE_RESTARTING;
  return NULL;
}

static void lgUpdate(void * data, obs_data_t * settings)
{
  LGPlugin * this = (LGPlugin *)data;

  deinit(this);
  this->shmFile = bstrdup(obs_data_get_string(settings, "shmFile"));
  if (!ivshmemOpenDev(&this->shmDev, this->shmFile))
    return;

  this->hideMouse = obs_data_get_bool(settings, "hideMouse") ? 1 : 0;
#if LIBOBS_API_MAJOR_VER >= 27
  this->dmabuf = obs_data_get_bool(settings, "dmabuf") &&
    ivshmemHasDMA(&this->shmDev);
#endif

  this->state = STATE_OPEN;

  uint32_t udataSize;
  KVMFR * udata;

  LGMP_STATUS status;
  if ((status = lgmpClientInit(this->shmDev.mem, this->shmDev.size,
      &this->lgmp)) != LGMP_OK)
  {
    printf("lgmpClientInit: %s\n", lgmpStatusString(status));
    return;
  }

  usleep(200000);

  uint32_t clientID;
  uint32_t remoteVersion;
  if ((status = lgmpClientSessionInit(this->lgmp, &udataSize,
      (uint8_t **)&udata, &clientID, &remoteVersion)) != LGMP_OK)
  {
    printf("lgmpClientSessionInit: %s", lgmpStatusString(status));
    if (status == LGMP_ERR_INVALID_VERSION)
    {
      printf("The host application is not compatible with this client\n");
      printf("Expected LGMP version %u but got %u\n",
          LGMP_PROTOCOL_VERSION, remoteVersion);
      printf("This is not a Looking Glass error, do not report this");
    }
    printf("\n");
    return;
  }

  if (udataSize < sizeof(KVMFR) ||
      memcmp(udata->magic, KVMFR_MAGIC, sizeof(udata->magic)) != 0 ||
      udata->version != KVMFR_VERSION)
  {
    printf("The host application is not compatible with this client\n");
    printf("Expected KVMFR version %d\n", KVMFR_VERSION);
    printf("This is not a Looking Glass error, do not report this\n");
    return;
  }

  lgFrameSchedulerInit(&this->frameScheduler,
    udata->features & KVMFR_FEATURE_FRAME_SCHEDULE, clientID);

  this->state = STATE_STARTING;
  createThreads(this);
}

#if LIBOBS_API_MAJOR_VER >= 27
static DMAFrameInfo * dmabufOpenDMAFrameInfo(LGPlugin * this, LGMPMessage * msg,
    const KVMFRFrame * frame, size_t dataSize)
{
  DMAFrameInfo * fi = NULL;

  /* find the existing dma buffer if it exists */
  for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
    if (this->dmaInfo[i].frame == frame)
      fi = &this->dmaInfo[i];

  /* if it's too small close it */
  if (fi && fi->dataSize < dataSize)
  {
    if (fi->texture)
    {
      gs_texture_destroy(fi->texture);
      fi->texture = NULL;
    }
    if (fi->fd >= 0)
      close(fi->fd);
    fi->fd = -1;
  }

  /* otherwise find a free buffer for use */
  if (!fi)
    for (int i = 0; i < ARRAY_LENGTH(this->dmaInfo); ++i)
      if (!this->dmaInfo[i].frame)
      {
        fi = &this->dmaInfo[i];
        fi->frame = frame;
        fi->fd    = -1;
        break;
      }

  if (!fi)
  {
    puts("Failed to obtain a free DMA frame slot");
    return NULL;
  }

  /* open the buffer */
  if (fi->fd == -1)
  {
    const uintptr_t pos    = (uintptr_t) msg->mem - (uintptr_t) this->shmDev.mem;
    const uintptr_t offset = (uintptr_t) frame->offset + sizeof(FrameBuffer);

    fi->dataSize = dataSize;
    fi->fd       = ivshmemGetDMABuf(&this->shmDev, pos + offset, dataSize);
    fi->texture  = NULL;
    if (fi->fd < 0)
    {
      puts("Failed to get the DMA buffer for the frame");
      return NULL;
    }
  }

  return fi;
}

static bool dmabufCreateTexture(LGPlugin * this, DMAFrameInfo * fi,
    const KVMFRFrame * frame)
{
  if (!fi)
    return false;

  fi->texture = gs_texture_create_from_dmabuf(
    this->dataWidth,
    this->dataHeight,
    this->drmFormat,
    this->format,
    1,
    &fi->fd,
    &(uint32_t) { frame->pitch },
    &(uint32_t) { 0 },
    NULL);

  return fi->texture != NULL;
}

static bool dmabufCopyTexture(LGPlugin * this, gs_texture_t * texture)
{
  gs_texrender_reset(this->dmaCopyRender);
  if (!gs_texrender_begin(
      this->dmaCopyRender, this->dataWidth, this->dataHeight))
    return false;

  gs_ortho(0.0f, (float)this->dataWidth,
    0.0f, (float)this->dataHeight, -100.0f, 100.0f);

  gs_effect_t * effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
  gs_eparam_t * image  = effect ?
    gs_effect_get_param_by_name(effect, "image") : NULL;
  if (!image)
  {
    gs_texrender_end(this->dmaCopyRender);
    return false;
  }

  /* EGLImages can use XRGB storage that is incompatible with GL copy-image.
   * A render pass samples across that storage boundary while preserving the
   * RGBA channels needed by the packed frame formats. */
  const bool framebufferSRGB = gs_framebuffer_srgb_enabled();
  gs_enable_framebuffer_srgb(false);
  gs_effect_set_texture(image, texture);
  gs_effect_set_next_sampler(image, this->dmaCopySampler);
  gs_blend_state_push();
  gs_enable_blending(false);
  while (gs_effect_loop(effect, "Draw"))
    gs_draw_sprite(texture, 0, this->dataWidth, this->dataHeight);
  gs_effect_set_texture(image, NULL);
  gs_blend_state_pop();
  gs_enable_framebuffer_srgb(framebufferSRGB);

  gs_texrender_end(this->dmaCopyRender);
  return gs_texrender_get_texture(this->dmaCopyRender) != NULL;
}
#endif

static bool mat3Inverse(const double m[9], double out[9])
{
  const double det =
    m[0] * (m[4] * m[8] - m[5] * m[7]) -
    m[1] * (m[3] * m[8] - m[5] * m[6]) +
    m[2] * (m[3] * m[7] - m[4] * m[6]);

  if (det == 0.0)
    return false;

  const double inv = 1.0 / det;
  out[0] = (m[4] * m[8] - m[5] * m[7]) * inv;
  out[1] = (m[2] * m[7] - m[1] * m[8]) * inv;
  out[2] = (m[1] * m[5] - m[2] * m[4]) * inv;
  out[3] = (m[5] * m[6] - m[3] * m[8]) * inv;
  out[4] = (m[0] * m[8] - m[2] * m[6]) * inv;
  out[5] = (m[2] * m[3] - m[0] * m[5]) * inv;
  out[6] = (m[3] * m[7] - m[4] * m[6]) * inv;
  out[7] = (m[1] * m[6] - m[0] * m[7]) * inv;
  out[8] = (m[0] * m[4] - m[1] * m[3]) * inv;
  return true;
}

static void mat3Mul(const double a[9], const double b[9], double out[9])
{
  for (int r = 0; r < 3; ++r)
    for (int c = 0; c < 3; ++c)
      out[r * 3 + c] =
        a[r * 3 + 0] * b[0 * 3 + c] +
        a[r * 3 + 1] * b[1 * 3 + c] +
        a[r * 3 + 2] * b[2 * 3 + c];
}

/* build the linear RGB -> CIE XYZ matrix for a set of primaries and white
 * point (Bruce Lindbloom's method) */
static bool primariesToXYZ(double xr, double yr, double xg, double yg,
    double xb, double yb, double xw, double yw, double out[9])
{
  if (yr == 0.0 || yg == 0.0 || yb == 0.0 || yw == 0.0)
    return false;

  const double Xr = xr / yr, Yr = 1.0, Zr = (1.0 - xr - yr) / yr;
  const double Xg = xg / yg, Yg = 1.0, Zg = (1.0 - xg - yg) / yg;
  const double Xb = xb / yb, Yb = 1.0, Zb = (1.0 - xb - yb) / yb;

  const double P[9] =
  {
    Xr, Xg, Xb,
    Yr, Yg, Yb,
    Zr, Zg, Zb
  };

  double Pinv[9];
  if (!mat3Inverse(P, Pinv))
    return false;

  const double Xw = xw / yw, Yw = 1.0, Zw = (1.0 - xw - yw) / yw;
  const double Sr = Pinv[0] * Xw + Pinv[1] * Yw + Pinv[2] * Zw;
  const double Sg = Pinv[3] * Xw + Pinv[4] * Yw + Pinv[5] * Zw;
  const double Sb = Pinv[6] * Xw + Pinv[7] * Yw + Pinv[8] * Zw;

  out[0] = Sr * Xr; out[1] = Sg * Xg; out[2] = Sb * Xb;
  out[3] = Sr * Yr; out[4] = Sg * Yg; out[5] = Sb * Yb;
  out[6] = Sr * Zr; out[7] = Sg * Zg; out[8] = Sb * Zb;
  return true;
}

/* Compute the linear-light matrix that converts BT.2020 to BT.709. HDR10
 * mastering-display primaries describe the display used to master the
 * content; they do not change the BT.2020 encoding gamut of the pixels. */
static void lgComputeColorMatrix(LGPlugin * this)
{
  static const double bt709[9] =
  {
    1.0, 0.0, 0.0,
    0.0, 1.0, 0.0,
    0.0, 0.0, 1.0
  };
  const double * result = bt709;
  double src[9], dst709[9], dst709inv[9], conv[9];

  /* BT.2020 / D65 source */
  const double xr = 0.7080, yr = 0.2920;
  const double xg = 0.1700, yg = 0.7970;
  const double xb = 0.1310, yb = 0.0460;
  const double xw = 0.3127, yw = 0.3290;

  /* BT.709 / D65 target */
  if (primariesToXYZ(xr, yr, xg, yg, xb, yb, xw, yw, src) &&
      primariesToXYZ(0.640, 0.330, 0.300, 0.600, 0.150, 0.060,
        0.3127, 0.3290, dst709) &&
      mat3Inverse(dst709, dst709inv))
  {
    mat3Mul(dst709inv, src, conv);
    result = conv;
  }
  else
    printf("Failed to compute BT.2020 to BT.709 color matrix\n");

  for (int r = 0; r < 3; ++r)
    vec3_set(&this->colorMatrix[r],
      (float)result[r * 3 + 0],
      (float)result[r * 3 + 1],
      (float)result[r * 3 + 2]);

  this->hdrScale = 10000.0f / LG_SCRGB_REFERENCE_WHITE;
}

static void lgFormatInit(LGPlugin * this, const KVMFRFrame * frame,
    LGMPMessage * msg, PLGMPClientQueue frameQueue)
{
  this->formatVer    = frame->formatVer;
  this->screenWidth  = frame->screenWidth;
  this->screenHeight = frame->screenHeight;
  this->dataWidth    = frame->dataWidth;
  this->dataHeight   = frame->dataHeight;
  this->frameWidth   = frame->frameWidth;
  this->frameHeight  = frame->frameHeight;
  this->type         = frame->type;

  this->screenScale.x = this->screenWidth  / this->frameWidth ;
  this->screenScale.y = this->screenHeight / this->frameHeight;

  obs_enter_graphics();
#if LIBOBS_API_MAJOR_VER >= 27
  dmabufInvalidateTextures(this);
  if (this->dmaCopyRender)
  {
    gs_texrender_destroy(this->dmaCopyRender);
    this->dmaCopyRender = NULL;
  }
#endif
  if (this->unpack && this->dstTexture)
  {
    gs_texture_destroy(this->dstTexture);
    this->dstTexture = NULL;
  }

  if (this->texture)
  {
    if (this->textureMapped)
      gs_texture_unmap(this->texture);

    gs_texture_destroy(this->texture);
    this->texture       = NULL;
    this->textureMapped = false;
  }
  this->texData  = NULL;
  this->linesize = 0;

  this->dataWidth     = frame->dataWidth;
  this->unpack        = false;
  this->hdr           = frame->flags & FRAME_FLAG_HDR;
  this->hdrPQ         = frame->flags & FRAME_FLAG_HDR_PQ;
  /* Keep the white level supplied with the cursor message. The frame value
   * is only a general SDR-content level and can differ from QueryHWCursor3. */

  if (this->hdr && this->hdrPQ)
    lgComputeColorMatrix(this);

  this->bpp = 4;
  switch(this->type)
  {
    case FRAME_TYPE_BGRA:
      this->format     = GS_BGRA_UNORM;
      this->drmFormat  = DRM_FORMAT_XRGB8888;
#if LIBOBS_API_MAJOR_VER >= 28
      this->colorSpace = GS_CS_SRGB;
#endif
      break;

    case FRAME_TYPE_RGBA:
      this->format     = GS_RGBA_UNORM;
      this->drmFormat  = DRM_FORMAT_XBGR8888;
#if LIBOBS_API_MAJOR_VER >= 28
      this->colorSpace = GS_CS_SRGB;
#endif
      break;

    case FRAME_TYPE_RGBA10:
      this->format     = GS_R10G10B10A2;
      this->drmFormat  = DRM_FORMAT_XBGR2101010;
#if LIBOBS_API_MAJOR_VER >= 28
      this->colorSpace = this->hdr ? GS_CS_709_SCRGB : GS_CS_SRGB;
#endif
      break;

    case FRAME_TYPE_RGB_24:
      this->bpp       = 3;
      this->dataWidth = frame->pitch / 4;
      /* fallthrough */

    case FRAME_TYPE_BGR_32:
      this->format    = GS_BGRA_UNORM;
      this->drmFormat = DRM_FORMAT_ARGB8888;
#if LIBOBS_API_MAJOR_VER >= 28
      this->colorSpace = GS_CS_SRGB;
#endif
      this->unpack     = true;
      break;

    case FRAME_TYPE_RGBA16F:
      this->bpp        = 8;
      this->format     = GS_RGBA16F;
      this->drmFormat  = DRM_FORMAT_XBGR16161616F;
#if LIBOBS_API_MAJOR_VER >= 28
      this->colorSpace = GS_CS_709_SCRGB;
#endif
      break;

    default:
      printf("invalid type %d\n", this->type);
      lgmpClientMessageDone(frameQueue);
      os_sem_post(this->frameSem);
      obs_leave_graphics();
      return;
  }

#if LIBOBS_API_MAJOR_VER >= 27
  if (this->dmabuf)
  {
    DMAFrameInfo * fi = dmabufOpenDMAFrameInfo(this, msg, frame,
        (size_t)frame->dataHeight * frame->pitch);
    if (fi && !fi->texture)
    {
      // create the first texture now so we can test if dmabuf is usable
      if (!dmabufCreateTexture(this, fi, frame))
      {
        puts("Failed to create dmabuf texture");
        this->dmabuf = false;
      }

      this->dmabufTested = true;
    }

    if (!this->dmaCopyTimer)
      this->dmaCopyTimer = gs_timer_create();
    if (!this->dmaCopyTimer)
    {
      puts("Failed to create DMA copy timer");
      this->dmabuf = false;
    }

    if (this->dmabuf && !this->dmaCopySampler)
    {
      const struct gs_sampler_info samplerInfo =
      {
        .filter    = GS_FILTER_POINT,
        .address_u = GS_ADDRESS_CLAMP,
        .address_v = GS_ADDRESS_CLAMP,
        .address_w = GS_ADDRESS_CLAMP,
      };
      this->dmaCopySampler = gs_samplerstate_create(&samplerInfo);
      if (!this->dmaCopySampler)
      {
        puts("Failed to create DMA copy sampler");
        this->dmabuf = false;
      }
    }
  }

  if (this->dmabuf)
  {
    this->dmaCopyRender =
      gs_texrender_create(this->format, GS_ZS_NONE);
    if (!this->dmaCopyRender)
    {
      puts("Failed to create DMA copy render target");
      this->dmabuf = false;
    }
  }
#else
  (void)drmFormat;
#endif

  if (!this->dmabuf)
  {
    this->texture = gs_texture_create(
      this->dataWidth,
      this->dataHeight,
      this->format,
      1,
      NULL,
      GS_DYNAMIC);

    if (!this->texture)
    {
      printf("create texture failed\n");
      lgmpClientMessageDone(frameQueue);
      os_sem_post(this->frameSem);
      obs_leave_graphics();
      return;
    }

    this->textureMapped =
      gs_texture_map(this->texture, &this->texData, &this->linesize);
    if (!this->textureMapped)
    {
      puts("Failed to map upload texture");
      gs_texture_destroy(this->texture);
      this->texture  = NULL;
      this->texData  = NULL;
      this->linesize = 0;
    }
  }

  if (this->unpack)
  {
    // create the render target for format unpacking
    this->dstTexture = gs_texture_create(
      this->frameWidth,
      this->frameHeight,
      GS_BGRA,
      1,
      NULL,
      GS_RENDER_TARGET);
  }

  obs_leave_graphics();
}

static void lgVideoTick(void * data, float seconds)
{
  LGPlugin * this = (LGPlugin *)data;
  (void)seconds;

  if (this->state == STATE_RESTARTING)
  {
    waitThreads(this);

    this->state = STATE_STARTING;
    createThreads(this);
  }
  if (this->state != STATE_RUNNING)
    return;

  const uint64_t tickTime    = os_gettime_ns();
  const uint64_t framePeriod = lgFramePeriod();
  LGMP_STATUS status;
  LGFrameMessage frameMessage;

  os_sem_wait(this->frameSem);
  if (this->state != STATE_RUNNING)
  {
    os_sem_post(this->frameSem);
    return;
  }

  const bool sourceActive = atomic_load_explicit(
    &this->sourceActive, memory_order_acquire);
  lgFrameSchedulerSetActive(&this->frameScheduler, sourceActive);
  if (sourceActive)
    lgFrameSchedulerSetPeriod(
      &this->frameScheduler, framePeriod, tickTime);

  this->cursorRect.x = this->cursor.x;
  this->cursorRect.y = this->cursor.y;

  /* update the cursor texture */
  unsigned int cursorVer = atomic_load(&this->cursorVer);
  if (cursorVer != this->cursorCurVer)
  {
    os_sem_wait(this->cursorSem);
    obs_enter_graphics();

    if (this->cursorTex)
    {
      gs_texture_destroy(this->cursorTex);
      this->cursorTex = NULL;
    }

    if (this->cursorXorTex)
    {
      gs_texture_destroy(this->cursorXorTex);
      this->cursorXorTex = NULL;
    }

    switch(this->cursor.type)
    {
      case CURSOR_TYPE_MASKED_COLOR:
      {
        const uint8_t * normal = (const uint8_t *)this->cursorData;
        const uint8_t * xorData = normal +
          this->cursor.width * this->cursor.height * sizeof(uint32_t);

        this->cursorMono   = false;
        this->cursorTex    = gs_texture_create(
            this->cursor.width,
            this->cursor.height,
            GS_BGRA,
            1,
            &normal,
            GS_DYNAMIC);
        this->cursorXorTex = gs_texture_create(
            this->cursor.width,
            this->cursor.height,
            GS_BGRA,
            1,
            &xorData,
            GS_DYNAMIC);
        break;
      }

      case CURSOR_TYPE_COLOR:
        this->cursorMono  = false;
        this->cursorTex   =
          gs_texture_create(
              this->cursor.width,
              this->cursor.height,
              GS_BGRA,
              1,
              (const uint8_t **)&this->cursorData,
              GS_DYNAMIC);
        break;

      case CURSOR_TYPE_MONOCHROME:
        this->cursorMono = true;
        this->cursorTex  =
          gs_texture_create(
              this->cursor.width,
              this->cursor.height,
              GS_BGRA,
              1,
              (const uint8_t **)&this->cursorData,
              GS_DYNAMIC);
        break;

      default:
        break;
    }

    obs_leave_graphics();

    this->cursorCurVer  = cursorVer;
    this->cursorRect.cx = this->cursor.width;
    this->cursorRect.cy = this->cursor.height;

    os_sem_post(this->cursorSem);
  }

  const unsigned int cursorTransformVer = atomic_load_explicit(
      &this->cursorTransformVer, memory_order_acquire);
  if (cursorTransformVer != this->cursorTransformCurVer)
  {
    os_sem_wait(this->cursorSem);
    this->activeCursorTransform = this->pendingCursorTransform;

    obs_enter_graphics();
    if (this->cursorLUT)
    {
      gs_effect_set_texture(this->cursorColorLUT, NULL);
      gs_texture_destroy(this->cursorLUT);
      this->cursorLUT = NULL;
    }

    if (this->activeCursorTransform.flags & KVMFR_COLOR_TRANSFORM_LUT)
    {
      const uint8_t * lut =
        (const uint8_t *)this->activeCursorTransform.lut;
      this->cursorLUT = gs_texture_create(
          4096, 1, GS_RGBA32F, 1, &lut, GS_DYNAMIC);
      if (!this->cursorLUT)
        blog(LOG_ERROR, "Failed to create cursor colour-transform LUT");
    }
    obs_leave_graphics();

    this->cursorTransformCurVer = cursorTransformVer;
    os_sem_post(this->cursorSem);
  }

  if ((status = lgFrameProcessNewest(this, &frameMessage)) != LGMP_OK)
  {
    if (status == LGMP_ERR_QUEUE_EMPTY)
    {
      os_sem_post(this->frameSem);
      return;
    }

    printf("lgFrameProcessNewest: %s\n", lgmpStatusString(status));
    this->state = STATE_STOPPING;
    os_sem_post(this->frameSem);
    return;
  }

  const KVMFRFrame * frame = (const KVMFRFrame *)frameMessage.msg.mem;
  const bool equalSerial = this->frameSerialValid &&
    frame->frameSerial == this->frameSerial;
  if (this->frameSerialValid && !equalSerial &&
      !lgFrameSerialNewer(frame->frameSerial, this->frameSerial))
  {
    lgmpClientMessageDone(frameMessage.queue);
    os_sem_post(this->frameSem);
    return;
  }
  if (equalSerial && !frameMessage.owner)
  {
    lgmpClientMessageDone(frameMessage.queue);
    os_sem_post(this->frameSem);
    return;
  }

  const FrameBuffer * fb =
    (const FrameBuffer *)((const uint8_t *)frame + frame->offset);
  bool frameComplete = false;
  if (frameMessage.owner)
  {
    frameComplete = framebuffer_wait(
      fb, (size_t)frame->dataHeight * frame->pitch);
    if (!frameComplete)
    {
      lgmpClientMessageDone(frameMessage.queue);
      os_sem_post(this->frameSem);
      return;
    }

    const uint64_t readyTime = os_gettime_ns();
    uint32_t generation;
    if (lgFramePhaseIdentity(&frameMessage, frame, &generation))
    {
      lgFrameSchedulerObserveFrame(&this->frameScheduler,
        frame->frameSerial, generation, frameMessage.scheduleEpoch,
        frameMessage.scheduleDeadlineSerial, readyTime);
      lgFrameSchedulerFeedback(&this->frameScheduler,
        frame->frameSerial, generation, frameMessage.scheduleEpoch,
        frameMessage.scheduleDeadlineSerial, tickTime);
    }
  }

  this->frameSerial      = frame->frameSerial;
  this->frameSerialValid = true;

  const bool textureValid = this->dmabuf ?
    this->dmabufTested && this->dmaCopyRender && this->dmaCopySampler &&
      this->dmaCopyTimer &&
      gs_texrender_get_texture(this->dmaCopyRender) :
    this->texture && this->textureMapped;
  if (!textureValid || this->formatVer != frame->formatVer)
    lgFormatInit(this, frame, &frameMessage.msg, frameMessage.queue);

#if LIBOBS_API_MAJOR_VER >= 27
  if (this->dmabuf)
  {
    obs_enter_graphics();
    DMAFrameInfo * fi = dmabufOpenDMAFrameInfo(this, &frameMessage.msg, frame,
      (size_t)frame->dataHeight * frame->pitch);
    bool importFailed = !fi;
    if (fi && !fi->texture)
      importFailed = !dmabufCreateTexture(this, fi, frame);
    obs_leave_graphics();

    if (!importFailed)
    {
      // Wait for the frame to be complete before copying it into OBS-owned
      // storage. The imported texture remains backed by reusable IVSHMEM.
      if (!frameComplete)
        frameComplete = framebuffer_wait(
          fb, (size_t)frame->dataHeight * frame->pitch);

      if (!frameComplete)
      {
        lgmpClientMessageDone(frameMessage.queue);
        os_sem_post(this->frameSem);
        return;
      }

      obs_enter_graphics();
      uint64_t copyTime;
      gs_timer_begin(this->dmaCopyTimer);
      const bool copied = dmabufCopyTexture(this, fi->texture);
      gs_timer_end(this->dmaCopyTimer);
      if (!gs_timer_get_data(this->dmaCopyTimer, &copyTime))
      {
        blog(LOG_ERROR, "Failed to wait for DMA frame copy");
        obs_leave_graphics();
        this->state = STATE_STOPPING;
        os_sem_post(this->frameSem);
        return;
      }
      obs_leave_graphics();

      if (copied)
      {
        lgmpClientMessageDone(frameMessage.queue);
        os_sem_post(this->frameSem);
        return;
      }

      importFailed = true;
    }

    puts("Failed to import or snapshot dmabuf texture, "
      "falling back to CPU upload");
    this->dmabuf = false;
    lgFormatInit(this, frame, &frameMessage.msg, frameMessage.queue);
  }
#endif

  if (!this->texture || !this->textureMapped || !this->texData)
  {
    lgmpClientMessageDone(frameMessage.queue);
    os_sem_post(this->frameSem);
    return;
  }

  framebuffer_read(
      fb,
      this->texData   , // dst
      this->linesize  , // dstpitch
      this->dataHeight, // height
      this->dataWidth , // width
      this->bpp       , // bpp
      frame->pitch
  );

  lgmpClientMessageDone(frameMessage.queue);
  os_sem_post(this->frameSem);

  obs_enter_graphics();
  if (this->textureMapped)
    gs_texture_unmap(this->texture);
  this->textureMapped = false;
  this->texData       = NULL;
  this->linesize      = 0;
  this->textureMapped =
    gs_texture_map(this->texture, &this->texData, &this->linesize);
  if (!this->textureMapped)
  {
    blog(LOG_ERROR, "Failed to remap upload texture");
    gs_texture_destroy(this->texture);
    this->texture = NULL;
  }
  obs_leave_graphics();
}

static void lgActivate(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  atomic_store_explicit(
    &this->sourceActive, true, memory_order_release);
}

static void lgDeactivate(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  atomic_store_explicit(
    &this->sourceActive, false, memory_order_release);
}

static void lgSetupCursorEffect(LGPlugin * this, int outputTransfer)
{
  const KVMFRColorTransform * transform = &this->activeCursorTransform;
  const bool matrixEnabled =
    transform->flags & KVMFR_COLOR_TRANSFORM_MATRIX;
  const bool lutEnabled =
    (transform->flags & KVMFR_COLOR_TRANSFORM_LUT) && this->cursorLUT;
  const float whiteLevel = atomic_load(&this->sdrWhiteLevel);

  gs_effect_set_float(this->cursorMultiplier,
      whiteLevel / LG_SCRGB_REFERENCE_WHITE);
  gs_effect_set_float(this->cursorSDRWhiteLevel, whiteLevel);
  gs_effect_set_int(this->cursorOutputTransfer, outputTransfer);
  gs_effect_set_bool(this->cursorMatrixEnabled, matrixEnabled);
  gs_effect_set_bool(this->cursorLUTEnabled, lutEnabled);
  for (int i = 0; i < 3; ++i)
    gs_effect_set_val(this->cursorColorMatrix[i],
        transform->matrix[i], sizeof(transform->matrix[i]));
  gs_effect_set_float(this->cursorColorScalar, transform->scalar);
  if (lutEnabled)
    gs_effect_set_texture(this->cursorColorLUT, this->cursorLUT);
}

static void lgVideoRender(void * data, gs_effect_t * effect)
{
  LGPlugin * this = (LGPlugin *)data;
  gs_texture_t * texture = this->texture;

#if LIBOBS_API_MAJOR_VER >= 27
  if (this->dmabuf)
    texture = this->dmaCopyRender ?
      gs_texrender_get_texture(this->dmaCopyRender) : NULL;
#endif

  if (!texture)
    return;

  const bool hdrDecode =
    this->type == FRAME_TYPE_RGBA10 && this->hdr && this->hdrPQ;

  if (this->type == FRAME_TYPE_RGB_24 || this->type == FRAME_TYPE_BGR_32)
  {
    effect = this->unpackEffect;
    gs_effect_set_texture(this->image, texture);
    struct vec2 outputSize;
    vec2_set(&outputSize, this->frameWidth, this->frameHeight);
    gs_effect_set_vec2(this->outputSize, &outputSize);
    gs_effect_set_int(this->swap, this->type == FRAME_TYPE_RGB_24 ? 1 : 0);
  }
  else if (hdrDecode)
  {
    /* decode the PQ (ST.2084) encoded frame to linear scRGB and remap the
     * source primaries to BT.709 so OBS receives valid GS_CS_709_SCRGB data */
    effect = this->hdrEffect;
    gs_effect_set_texture(this->hdrImage, texture);
    struct vec2 outputSize;
    vec2_set(&outputSize, this->frameWidth, this->frameHeight);
    gs_effect_set_vec2(this->hdrOutputSize, &outputSize);
    for (int i = 0; i < 3; ++i)
      gs_effect_set_vec3(this->hdrColorMatrix[i], &this->colorMatrix[i]);
    gs_effect_set_float(this->hdrScaleParam, this->hdrScale);
  }
  else
  {
    effect = obs_get_base_effect(OBS_EFFECT_OPAQUE);
    gs_eparam_t * image = gs_effect_get_param_by_name(effect, "image");
    gs_effect_set_texture(image, texture);
  }

  if (this->unpack)
    texture = this->dstTexture;

  while (gs_effect_loop(effect, "Draw"))
    gs_draw_sprite(texture, 0, 0, 0);

  if (this->cursorVisible && this->cursorTex)
  {
    struct matrix4 m4;
    gs_matrix_get(&m4);
    struct gs_rect r =
    {
      .x  = m4.t.x,
      .y  = m4.t.y,
      .cx = (double)this->frameWidth  * m4.x.x,
      .cy = (double)this->frameHeight * m4.y.y
    };
    gs_set_scissor_rect(&r);

    /* Color cursor pixels are premultiplied sRGB. When rendering into OBS's
     * linear scRGB space, decode them and reproduce the source display's SDR
     * white level reported by the IDD. */
    bool mapCursorToScRGB = false;
#if LIBOBS_API_MAJOR_VER >= 28
    mapCursorToScRGB = this->colorSpace == GS_CS_709_SCRGB;
#endif

    const bool hasCursorTransform =
      this->activeCursorTransform.flags != 0;
    const bool useCursorEffect = !this->cursorMono &&
      (mapCursorToScRGB || hasCursorTransform);
    const int cursorOutputTransfer = !mapCursorToScRGB ? 0 :
      (this->hdrPQ ? 2 : 1);

    effect = useCursorEffect ?
      this->cursorEffect : obs_get_base_effect(OBS_EFFECT_DEFAULT);
    gs_eparam_t * image = useCursorEffect ?
      this->cursorImage : gs_effect_get_param_by_name(effect, "image");
    gs_effect_set_texture(image, this->cursorTex);
    if (useCursorEffect)
      lgSetupCursorEffect(this, cursorOutputTransfer);

    gs_matrix_push();
    gs_matrix_translate3f(
        this->cursorRect.x / this->screenScale.x,
        this->cursorRect.y / this->screenScale.y,
        0.0f);

    gs_blend_state_push();
    gs_enable_blending(true);

    if (!this->cursorMono)
    {
      gs_blend_function_separate(
          GS_BLEND_ONE, GS_BLEND_INVSRCALPHA,
          GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
      while (gs_effect_loop(effect, "Draw"))
        gs_draw_sprite(this->cursorTex, 0, 0, 0);

      if (this->cursor.type == CURSOR_TYPE_MASKED_COLOR &&
          this->cursorXorTex)
      {
        /* The XOR plane is a logical operand, not SDR colour. Applying the
         * cursor colour transform or white-level mapping changes the mask
         * before the inverse-destination blend. */
        effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
        image  = gs_effect_get_param_by_name(effect, "image");
        gs_effect_set_texture(image, this->cursorXorTex);
        gs_blend_function_separate(
            GS_BLEND_INVDSTCOLOR, GS_BLEND_INVSRCALPHA,
            GS_BLEND_ZERO       , GS_BLEND_ONE);
        while (gs_effect_loop(effect, "Draw"))
          gs_draw_sprite(this->cursorXorTex, 0, 0, 0);
      }
    }
    else
    {
      gs_blend_function_separate(
          GS_BLEND_INVDSTCOLOR, GS_BLEND_INVSRCALPHA,
          GS_BLEND_ONE        , GS_BLEND_INVSRCALPHA);
      while (gs_effect_loop(effect, "Draw"))
        gs_draw_sprite(this->cursorTex, 0, 0, 0);
    }

    gs_blend_state_pop();
    gs_matrix_pop();
    gs_set_scissor_rect(NULL);
  }
}

#if LIBOBS_API_MAJOR_VER >= 28
static enum gs_color_space lgVideoGetColorSpace(void *data, size_t count,
  const enum gs_color_space *preferred_spaces)
{
  LGPlugin * this = (LGPlugin *)data;
  return this->colorSpace;
}
#endif

static uint32_t lgGetWidth(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  return this->frameWidth;
}

static uint32_t lgGetHeight(void * data)
{
  LGPlugin * this = (LGPlugin *)data;
  return this->frameHeight;
}

struct obs_source_info lg_source =
{
  .id                    = "looking-glass-obs",
  .type                  = OBS_SOURCE_TYPE_INPUT,
  .output_flags          = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |
                           OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB,
  .get_name              = lgGetName,
  .create                = lgCreate,
  .destroy               = lgDestroy,
  .update                = lgUpdate,
  .get_defaults          = lgGetDefaults,
  .get_properties        = lgGetProperties,
  .activate              = lgActivate,
  .deactivate            = lgDeactivate,
  .video_tick            = lgVideoTick,
  .video_render          = lgVideoRender,
#if LIBOBS_API_MAJOR_VER >= 28
  .video_get_color_space = lgVideoGetColorSpace,
#endif
  .get_width             = lgGetWidth,
  .get_height            = lgGetHeight,
  .icon_type             = OBS_ICON_TYPE_DESKTOP_CAPTURE
};
