/**
 * 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/transport.h"

#include "common/KVMFR.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include "common/ivshmem.h"
#include "common/locking.h"
#include "common/option.h"
#include "common/stringutils.h"

#include <lgmp/client.h>

#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define LGMP_TIMING_SPIN_COUNT 4096

struct DMAFrameInfo
{
  const KVMFRFrame * frame;
  size_t             dataSize;
  int                fd;
};

struct LG_Transport
{
  struct IVSHMEM    shm;
  PLGMPClient       client;
  PLGMPClientQueue  frameQueue;
  PLGMPClientQueue  ownerFrameQueue[LGMP_Q_FRAME_LEN];
  PLGMPClientQueue  pendingFrameQueue;
  PLGMPClientQueue  pointerQueue;
  LG_Lock           pointerLock;

  unsigned cursorPollInterval;
  unsigned framePollInterval;
  bool     allowDMA;
  bool     connected;
  bool     framePending;
  bool     frameScheduleSupported;
  const KVMFRFrame * pendingFrame;
  uint32_t clientID;
  uint32_t frameSerial;
  bool     frameSerialValid;
  bool     formatValid;
  LG_TransportFrameFormat format;

  struct DMAFrameInfo dma[LGMP_Q_FRAME_BUFFER_LEN];
  uint8_t            * pointerData;
  size_t               pointerDataSize;
};

static bool lgmp_deviceValidator(struct Option * opt, const char ** error)
{
  const char * transport = option_get_string("app", "transport");
  if (!transport || strcmp(transport, "lgmp") != 0)
    return true;

  if (strlen(opt->value.x_string) > 3 &&
      memcmp(opt->value.x_string, "kvmfr", 5) != 0)
  {
    struct stat st;
    if (stat(opt->value.x_string, &st) != 0)
    {
      *error = "Invalid path to the shared memory file";
      return false;
    }
  }
  return true;
}

static StringList lgmp_deviceValues(struct Option * option)
{
  StringList values = stringlist_new(true);
  DIR * dir = opendir("/sys/class/kvmfr");
  if (!dir)
    return values;

  struct dirent * entry;
  while ((entry = readdir(dir)))
  {
    if (entry->d_name[0] == '.')
      continue;
    char * name;
    alloc_sprintf(&name, "/dev/%s", entry->d_name);
    stringlist_push(values, name);
  }
  closedir(dir);
  return values;
}

static void lgmp_setup(void)
{
  struct stat st;
  const char * defaultDevice = stat("/dev/kvmfr0", &st) == 0 ?
    "/dev/kvmfr0" : "/dev/shm/looking-glass";

  static struct Option options[] =
  {
    {
      .module         = "lgmp",
      .name           = "shmDevice",
      .old_module     = "app",
      .old_name       = "shmFile",
      .shortopt       = 'f',
      .description    = "Shared memory file or KVMFR device path",
      .type           = OPTION_TYPE_STRING,
      .value.x_string = NULL,
      .validator      = lgmp_deviceValidator,
      .getValues      = lgmp_deviceValues,
    },
    {
      .module       = "lgmp",
      .name         = "allowDMA",
      .old_module   = "app",
      .old_name     = "allowDMA",
      .description  = "Allow direct DMA transfers when supported",
      .type         = OPTION_TYPE_BOOL,
      .value.x_bool = true,
    },
    {
      .module      = "lgmp",
      .name        = "framePollInterval",
      .old_module  = "app",
      .old_name    = "framePollInterval",
      .description = "Frame queue polling interval in microseconds",
      .type        = OPTION_TYPE_INT,
      .value.x_int = 1000,
    },
    {
      .module      = "lgmp",
      .name        = "cursorPollInterval",
      .old_module  = "app",
      .old_name    = "cursorPollInterval",
      .description = "Pointer queue polling interval in microseconds",
      .type        = OPTION_TYPE_INT,
      .value.x_int = 1000,
    },
    {0}
  };

  options[0].value.x_string = (char *)defaultDevice;
  option_register(options);
}

static bool lgmp_create(LG_Transport ** result)
{
  struct LG_Transport * this = calloc(1, sizeof(*this));
  if (!this)
    return false;

  this->allowDMA       = option_get_bool("lgmp", "allowDMA");
  const int framePoll  = option_get_int("lgmp", "framePollInterval");
  const int cursorPoll = option_get_int("lgmp", "cursorPollInterval");
  if (framePoll < 0 || cursorPoll < 0)
  {
    DEBUG_ERROR("LGMP polling intervals cannot be negative");
    free(this);
    return false;
  }
  this->framePollInterval  = framePoll;
  this->cursorPollInterval = cursorPoll;

  for (unsigned i = 0; i < LGMP_Q_FRAME_BUFFER_LEN; ++i)
    this->dma[i].fd = -1;

  LG_LOCK_INIT(this->pointerLock);

  if (!ivshmemOpenDev(&this->shm,
        option_get_string("lgmp", "shmDevice")))
  {
    LG_LOCK_FREE(this->pointerLock);
    free(this);
    return false;
  }

  LGMP_STATUS status = lgmpClientInit(this->shm.mem, this->shm.size,
      &this->client);
  if (status != LGMP_OK)
  {
    DEBUG_ERROR("lgmpClientInit failed: %s", lgmpStatusString(status));
    ivshmemClose(&this->shm);
    LG_LOCK_FREE(this->pointerLock);
    free(this);
    return false;
  }

  *result = this;
  return true;
}

static void lgmp_stopFrame(struct LG_Transport * this)
{
  if (this->framePending && this->pendingFrameQueue)
  {
    const LGMP_STATUS status =
      lgmpClientMessageDone(this->pendingFrameQueue);
    if (status != LGMP_OK)
      DEBUG_WARN("Failed to release pending LGMP frame: %s",
          lgmpStatusString(status));
  }
  this->framePending      = false;
  this->pendingFrame      = NULL;
  this->pendingFrameQueue = NULL;

  LGMP_STATUS status = lgmpClientUnsubscribe(&this->frameQueue);
  if (status != LGMP_OK)
  {
    DEBUG_WARN("Failed to unsubscribe from the shared LGMP frame queue: %s",
        lgmpStatusString(status));
    this->frameQueue = NULL;
  }
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
  {
    status = lgmpClientUnsubscribe(&this->ownerFrameQueue[i]);
    if (status != LGMP_OK)
    {
      DEBUG_WARN("Failed to unsubscribe from owner LGMP frame queue %u: %s",
          i, lgmpStatusString(status));
      this->ownerFrameQueue[i] = NULL;
    }
  }

  this->frameSerial      = 0;
  this->frameSerialValid = false;
  this->formatValid      = false;
}

static void lgmp_stopPointer(struct LG_Transport * this)
{
  LG_LOCK(this->pointerLock);
  const LGMP_STATUS status = lgmpClientUnsubscribe(&this->pointerQueue);
  if (status != LGMP_OK)
  {
    DEBUG_WARN("Failed to unsubscribe from the LGMP pointer queue: %s",
        lgmpStatusString(status));
    this->pointerQueue = NULL;
  }
  LG_UNLOCK(this->pointerLock);
}

static void lgmp_closeQueues(struct LG_Transport * this)
{
  lgmp_stopFrame(this);
  lgmp_stopPointer(this);
}

static void lgmp_closeDMA(struct LG_Transport * this)
{
  for (unsigned i = 0; i < LGMP_Q_FRAME_BUFFER_LEN; ++i)
  {
    if (this->dma[i].fd >= 0)
      close(this->dma[i].fd);
    this->dma[i].fd       = -1;
    this->dma[i].frame    = NULL;
    this->dma[i].dataSize = 0;
  }
}

static void lgmp_destroy(LG_Transport ** transport)
{
  if (!transport || !*transport)
    return;

  struct LG_Transport * this = *transport;
  lgmp_closeQueues(this);
  lgmp_closeDMA(this);
  free(this->pointerData);
  lgmpClientFree(&this->client);
  ivshmemClose(&this->shm);
  LG_LOCK_FREE(this->pointerLock);
  free(this);
  *transport = NULL;
}

static bool lgmp_parseSession(const uint8_t * data, uint32_t size,
    LG_TransportSession * session)
{
  if (size < sizeof(KVMFR))
    return false;

  const KVMFR * header = (const KVMFR *)data;
  if (memcmp(header->magic, KVMFR_MAGIC, sizeof(header->magic)) != 0 ||
      header->version != KVMFR_VERSION)
  {
    session->remoteVersion = header->version;
    return false;
  }

  str_copy(session->version, sizeof(session->version), header->hostver,
      sizeof(header->hostver));
  if (header->features & KVMFR_FEATURE_SETCURSORPOS)
    session->features |= LG_TRANSPORT_FEATURE_SET_CURSOR_POS;
  if (header->features & KVMFR_FEATURE_WINDOWSIZE)
    session->features |= LG_TRANSPORT_FEATURE_WINDOW_SIZE;
  if (header->features & KVMFR_FEATURE_FRAME_SCHEDULE)
    session->features |= LG_TRANSPORT_FEATURE_FRAME_SCHEDULE;

  data += sizeof(*header);
  size -= sizeof(*header);
  while (size >= sizeof(KVMFRRecord))
  {
    const KVMFRRecord * record = (const KVMFRRecord *)data;
    data += sizeof(*record);
    size -= sizeof(*record);
    if (record->size > size)
      break;

    switch (record->type)
    {
      case KVMFR_RECORD_VMINFO:
        if (record->size >= sizeof(KVMFRRecord_VMInfo))
        {
          const KVMFRRecord_VMInfo * info =
            (const KVMFRRecord_VMInfo *)data;
          memcpy(session->uuid, info->uuid, sizeof(session->uuid));
          for (unsigned i = 0; i < sizeof(session->uuid); ++i)
            session->uuidValid |= session->uuid[i] != 0;
          str_copy(session->capture, sizeof(session->capture), info->capture,
              sizeof(info->capture));
          session->cpus    = info->cpus;
          session->cores   = info->cores;
          session->sockets = info->sockets;
          const size_t fixed = offsetof(KVMFRRecord_VMInfo, model);
          str_copy(session->cpuModel, sizeof(session->cpuModel), info->model,
              record->size - fixed);
        }
        break;

      case KVMFR_RECORD_OSINFO:
        if (record->size >= sizeof(KVMFRRecord_OSInfo))
        {
          const KVMFRRecord_OSInfo * info =
            (const KVMFRRecord_OSInfo *)data;
          session->os = info->os <= KVMFR_OS_OTHER ?
            (LG_TransportGuestOS)info->os : LG_TRANSPORT_OS_OTHER;
          const size_t fixed = offsetof(KVMFRRecord_OSInfo, name);
          str_copy(session->osName, sizeof(session->osName), info->name,
              record->size - fixed);
        }
        break;
    }

    data += record->size;
    size -= record->size;
  }

  return true;
}

static LG_TransportStatus lgmp_connect(LG_Transport * this,
    LG_TransportSession * session)
{
  memset(session, 0, sizeof(*session));
  session->os = LG_TRANSPORT_OS_OTHER;

  uint32_t size;
  uint8_t * data;
  uint32_t remoteVersion;
  LGMP_STATUS status = lgmpClientSessionInit(this->client, &size, &data,
      &this->clientID, &remoteVersion);
  session->remoteVersion = remoteVersion;
  switch (status)
  {
    case LGMP_OK:
      if (!lgmp_parseSession(data, size, session))
        return LG_TRANSPORT_INVALID_VERSION;
      this->connected              = true;
      this->frameScheduleSupported =
        session->features & LG_TRANSPORT_FEATURE_FRAME_SCHEDULE;
      this->frameSerial            = 0;
      this->frameSerialValid       = false;
      this->formatValid            = false;
      return LG_TRANSPORT_OK;

    case LGMP_ERR_INVALID_VERSION:
      return LG_TRANSPORT_INVALID_VERSION;

    case LGMP_ERR_INVALID_SESSION:
    case LGMP_ERR_INVALID_MAGIC:
      return LG_TRANSPORT_UNAVAILABLE;

    default:
      DEBUG_ERROR("lgmpClientSessionInit failed: %s", lgmpStatusString(status));
      return LG_TRANSPORT_ERROR;
  }
}

static void lgmp_disconnect(LG_Transport * this)
{
  lgmp_closeQueues(this);
  lgmp_closeDMA(this);
  this->connected              = false;
  this->frameScheduleSupported = false;
  this->clientID               = 0;
}

static bool lgmp_sessionValid(LG_Transport * this)
{
  return this->connected && lgmpClientSessionValid(this->client);
}

static bool lgmp_supportsDMA(LG_Transport * this)
{
  return this->allowDMA && ivshmemHasDMA(&this->shm);
}

static bool lgmp_attachRenderer(LG_Transport * this,
    const LG_RendererInterop * interop)
{
  return true;
}

static void lgmp_detachRenderer(LG_Transport * this)
{
}

static LG_TransportStatus lgmp_subscribe(PLGMPClient client, uint32_t id,
    PLGMPClientQueue * queue)
{
  if (*queue)
    return LG_TRANSPORT_OK;

  PLGMPClientQueue subscribed = NULL;
  LGMP_STATUS status = lgmpClientSubscribe(client, id, &subscribed);
  switch (status)
  {
    case LGMP_OK:
      *queue = subscribed;
      return LG_TRANSPORT_OK;
    case LGMP_ERR_NO_SUCH_QUEUE: return LG_TRANSPORT_TIMEOUT;
    case LGMP_ERR_INVALID_SESSION: return LG_TRANSPORT_DISCONNECTED;
    default:
      DEBUG_ERROR("lgmpClientSubscribe failed: %s", lgmpStatusString(status));
      return LG_TRANSPORT_ERROR;
  }
}

static LG_TransportStatus lgmp_process(PLGMPClientQueue queue,
    unsigned interval, LGMPMessage * message)
{
  LGMP_STATUS status = lgmpClientProcess(queue, message);
  switch (status)
  {
    case LGMP_OK:
      return LG_TRANSPORT_OK;
    case LGMP_ERR_QUEUE_EMPTY:
      if (interval)
        usleep(interval);
      return LG_TRANSPORT_TIMEOUT;
    case LGMP_ERR_INVALID_SESSION:
      return LG_TRANSPORT_DISCONNECTED;
    default:
      DEBUG_ERROR("lgmpClientProcess failed: %s", lgmpStatusString(status));
      return LG_TRANSPORT_ERROR;
  }
}

struct LGMPFrameMessage
{
  PLGMPClientQueue   queue;
  LGMPMessage        message;
  const KVMFRFrame * frame;
  bool               owner;
};

static LG_TransportStatus lgmp_pollFrameQueue(PLGMPClientQueue queue,
    bool owner, struct LGMPFrameMessage * result)
{
  const LGMP_STATUS advance = lgmpClientAdvanceToLast(queue);
  switch (advance)
  {
    case LGMP_OK:
      break;
    case LGMP_ERR_QUEUE_EMPTY:
      break;
    case LGMP_ERR_INVALID_SESSION:
      return LG_TRANSPORT_DISCONNECTED;
    default:
      DEBUG_ERROR("lgmpClientAdvanceToLast failed: %s",
          lgmpStatusString(advance));
      return LG_TRANSPORT_ERROR;
  }

  const LG_TransportStatus status = lgmp_process(queue, 0, &result->message);
  if (status == LG_TRANSPORT_OK)
  {
    result->queue = queue;
    result->owner = owner || result->message.udata != 0;
  }
  return status;
}

static LG_TransportStatus lgmp_doneFrameMessage(
    struct LGMPFrameMessage * message)
{
  if (!message || !message->queue)
    return LG_TRANSPORT_OK;

  const LGMP_STATUS status = lgmpClientMessageDone(message->queue);
  message->queue = NULL;
  message->frame = NULL;

  if (status == LGMP_OK)
    return LG_TRANSPORT_OK;
  if (status == LGMP_ERR_INVALID_SESSION)
    return LG_TRANSPORT_DISCONNECTED;

  DEBUG_WARN("Failed to release discarded LGMP frame: %s",
      lgmpStatusString(status));
  return LG_TRANSPORT_ERROR;
}

static void lgmp_mergeFrameStatus(LG_TransportStatus status,
    LG_TransportStatus * result)
{
  if (status != LG_TRANSPORT_OK &&
      (*result == LG_TRANSPORT_OK ||
       status == LG_TRANSPORT_DISCONNECTED))
    *result = status;
}

static bool lgmp_validateFrameMessage(struct LGMPFrameMessage * message)
{
  if (message->message.size < sizeof(KVMFRFrame))
  {
    DEBUG_ERROR("LGMP frame payload is too small");
    return false;
  }

  const KVMFRFrame * frame = (const KVMFRFrame *)message->message.mem;
  const size_t frameDataSize = (size_t)frame->dataHeight * frame->pitch;
  if (frame->type <= FRAME_TYPE_INVALID || frame->type >= FRAME_TYPE_MAX ||
      frame->offset > message->message.size - sizeof(FrameBuffer) ||
      frameDataSize >
        message->message.size - frame->offset - sizeof(FrameBuffer))
  {
    DEBUG_ERROR("LGMP frame payload contains invalid dimensions or offsets");
    return false;
  }

  message->frame = frame;
  return true;
}

static bool lgmp_frameSerialNewer(uint32_t lhs, uint32_t rhs)
{
  return lhs != rhs &&
    (uint32_t)(lhs - rhs) < UINT32_C(0x80000000);
}

static void lgmp_selectNewestFrameMessage(
    struct LGMPFrameMessage * candidate,
    struct LGMPFrameMessage ** selected)
{
  if (!candidate->frame)
    return;

  if (!*selected ||
      lgmp_frameSerialNewer(candidate->frame->frameSerial,
        (*selected)->frame->frameSerial) ||
      (candidate->frame->frameSerial == (*selected)->frame->frameSerial &&
       candidate->owner && !(*selected)->owner))
    *selected = candidate;
}

static int lgmp_getDMA(struct LG_Transport * this, const KVMFRFrame * frame,
    size_t dataSize)
{
  struct DMAFrameInfo * dma = NULL;
  for (unsigned i = 0; i < LGMP_Q_FRAME_BUFFER_LEN; ++i)
    if (this->dma[i].frame == frame)
    {
      dma = &this->dma[i];
      if (dma->dataSize < dataSize && dma->fd >= 0)
      {
        close(dma->fd);
        dma->fd = -1;
      }
      break;
    }

  if (!dma)
    for (unsigned i = 0; i < LGMP_Q_FRAME_BUFFER_LEN; ++i)
      if (!this->dma[i].frame)
      {
        dma = &this->dma[i];
        dma->frame = frame;
        break;
      }

  if (!dma)
    return -1;
  if (dma->fd >= 0)
    return dma->fd;

  const uintptr_t position = (uintptr_t)frame - (uintptr_t)this->shm.mem;
  const uintptr_t offset = frame->offset + sizeof(FrameBuffer);
  dma->dataSize = dataSize;
  dma->fd       = ivshmemGetDMABuf(&this->shm, position + offset, dataSize);
  return dma->fd;
}

static LG_TransportStatus lgmp_nextFrame(LG_Transport * this, bool useDMA,
    LG_TransportFrame * result)
{
  if (!this->connected)
    return LG_TRANSPORT_DISCONNECTED;
  if (this->framePending)
    return LG_TRANSPORT_ERROR;

  if (this->frameScheduleSupported)
    for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    {
      const LG_TransportStatus status = lgmp_subscribe(this->client,
          LGMP_Q_FRAME_OWNER + i, &this->ownerFrameQueue[i]);
      if (status != LG_TRANSPORT_OK && status != LG_TRANSPORT_TIMEOUT)
        return status;
    }

  const LG_TransportStatus sharedSubscribe = lgmp_subscribe(this->client,
      LGMP_Q_FRAME, &this->frameQueue);
  if (sharedSubscribe != LG_TRANSPORT_OK &&
      sharedSubscribe != LG_TRANSPORT_TIMEOUT)
    return sharedSubscribe;

  struct LGMPFrameMessage owner[LGMP_Q_FRAME_LEN] = {};
  struct LGMPFrameMessage shared = {};
  LG_TransportStatus sharedStatus = LG_TRANSPORT_TIMEOUT;
  LG_TransportStatus ownerStatus[LGMP_Q_FRAME_LEN];

  /* Select the newest frame across the shared and owner delivery lanes. */
  if (this->frameQueue)
    sharedStatus = lgmp_pollFrameQueue(this->frameQueue, false, &shared);
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
  {
    ownerStatus[i] = LG_TRANSPORT_TIMEOUT;
    if (this->ownerFrameQueue[i])
      ownerStatus[i] = lgmp_pollFrameQueue(
          this->ownerFrameQueue[i], true, &owner[i]);
  }

  LG_TransportStatus failure = LG_TRANSPORT_OK;
  if (sharedStatus != LG_TRANSPORT_OK &&
      sharedStatus != LG_TRANSPORT_TIMEOUT)
    failure = sharedStatus;
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    if (ownerStatus[i] != LG_TRANSPORT_OK &&
        ownerStatus[i] != LG_TRANSPORT_TIMEOUT &&
        (failure == LG_TRANSPORT_OK ||
         ownerStatus[i] == LG_TRANSPORT_DISCONNECTED))
      failure = ownerStatus[i];

  if (failure != LG_TRANSPORT_OK)
  {
    lgmp_mergeFrameStatus(
        lgmp_doneFrameMessage(&shared), &failure);
    for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
      lgmp_mergeFrameStatus(
          lgmp_doneFrameMessage(&owner[i]), &failure);
    return failure;
  }

  bool malformed = false;
  LG_TransportStatus releaseFailure = LG_TRANSPORT_OK;
  if (sharedStatus == LG_TRANSPORT_OK &&
      !lgmp_validateFrameMessage(&shared))
  {
    malformed = true;
    releaseFailure = lgmp_doneFrameMessage(&shared);
  }
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    if (ownerStatus[i] == LG_TRANSPORT_OK &&
        !lgmp_validateFrameMessage(&owner[i]))
    {
      malformed = true;
      const LG_TransportStatus done =
        lgmp_doneFrameMessage(&owner[i]);
      lgmp_mergeFrameStatus(done, &releaseFailure);
    }

  if (releaseFailure != LG_TRANSPORT_OK)
  {
    lgmp_mergeFrameStatus(
        lgmp_doneFrameMessage(&shared), &releaseFailure);
    for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
      lgmp_mergeFrameStatus(
          lgmp_doneFrameMessage(&owner[i]), &releaseFailure);
    return releaseFailure;
  }

  struct LGMPFrameMessage * selected = NULL;
  lgmp_selectNewestFrameMessage(&shared, &selected);
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    lgmp_selectNewestFrameMessage(&owner[i], &selected);

  if (!selected)
  {
    if (this->framePollInterval)
      usleep(this->framePollInterval);
    return malformed ? LG_TRANSPORT_ERROR : LG_TRANSPORT_TIMEOUT;
  }

  if (selected != &shared)
    releaseFailure = lgmp_doneFrameMessage(&shared);
  for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i)
    if (selected != &owner[i])
    {
      const LG_TransportStatus done =
        lgmp_doneFrameMessage(&owner[i]);
      lgmp_mergeFrameStatus(done, &releaseFailure);
    }

  if (releaseFailure != LG_TRANSPORT_OK)
  {
    lgmp_mergeFrameStatus(
        lgmp_doneFrameMessage(selected), &releaseFailure);
    return releaseFailure;
  }

  const KVMFRFrame * frame = selected->frame;
  const bool equalSerial = this->frameSerialValid &&
    frame->frameSerial == this->frameSerial;
  if (this->frameSerialValid && !equalSerial &&
      !lgmp_frameSerialNewer(frame->frameSerial, this->frameSerial))
  {
    const LG_TransportStatus done = lgmp_doneFrameMessage(selected);
    return done == LG_TRANSPORT_OK ? LG_TRANSPORT_TIMEOUT : done;
  }
  if (equalSerial && !selected->owner)
  {
    const LG_TransportStatus done = lgmp_doneFrameMessage(selected);
    return done == LG_TRANSPORT_OK ? LG_TRANSPORT_TIMEOUT : done;
  }

  const bool fullDamage = !this->frameSerialValid || equalSerial ||
    frame->frameSerial != this->frameSerial + 1;
  this->frameSerial      = frame->frameSerial;
  this->frameSerialValid = true;

  memset(result, 0, sizeof(*result));
  result->serial = frame->frameSerial;
  if (selected->owner)
  {
    const uint64_t scheduleToken = selected->message.udata;
    result->scheduleGeneration   = (uint32_t)(scheduleToken >> 32);
    result->scheduleEpoch        = (uint32_t)scheduleToken;
    result->scheduleOwner        = true;
  }
  if (frame->flags & FRAME_FLAG_BLOCK_SCREENSAVER)
    result->flags |= LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER;
  if (frame->flags & FRAME_FLAG_REQUEST_ACTIVATION)
    result->flags |= LG_TRANSPORT_FRAME_REQUEST_ACTIVATION;
  if (frame->flags & FRAME_FLAG_TRUNCATED)
    result->flags |= LG_TRANSPORT_FRAME_TRUNCATED;

  LG_TransportFrameFormat * format = &this->format;
  if (!this->formatValid || format->version != frame->formatVer)
  {
    memset(format, 0, sizeof(*format));
    format->version       = frame->formatVer;
    format->type          = frame->type;
    format->screenWidth   = frame->screenWidth;
    format->screenHeight  = frame->screenHeight;
    format->dataWidth     = frame->dataWidth;
    format->dataHeight    = frame->dataHeight;
    format->frameWidth    = frame->frameWidth;
    format->frameHeight   = frame->frameHeight;
    format->rotation      = frame->rotation;
    format->stride        = frame->stride;
    format->pitch         = frame->pitch;
    format->hdr           = frame->flags & FRAME_FLAG_HDR;
    format->hdrPQ         = frame->flags & FRAME_FLAG_HDR_PQ;
    format->hdrMetadata   = frame->flags & FRAME_FLAG_HDR_METADATA;
    format->sdrWhiteLevel = frame->sdrWhiteLevel ? frame->sdrWhiteLevel :
      KVMFR_SDR_WHITE_LEVEL_DEFAULT;
    if (format->hdrMetadata)
    {
      memcpy(format->hdrDisplayPrimary, frame->hdrDisplayPrimary,
          sizeof(format->hdrDisplayPrimary));
      memcpy(format->hdrWhitePoint, frame->hdrWhitePoint,
          sizeof(format->hdrWhitePoint));
      format->hdrMaxDisplayLuminance       = frame->hdrMaxDisplayLuminance;
      format->hdrMinDisplayLuminance       = frame->hdrMinDisplayLuminance;
      format->hdrMaxContentLightLevel      = frame->hdrMaxContentLightLevel;
      format->hdrMaxFrameAverageLightLevel =
        frame->hdrMaxFrameAverageLightLevel;
    }
    this->formatValid = true;
  }
  result->format = format;

  result->framebuffer = (const FrameBuffer *)((const uint8_t *)frame +
      frame->offset);
  result->dmaFD       = -1;
  if (useDMA)
  {
    const size_t dataSize = (size_t)format->dataHeight * format->pitch;
    result->dmaFD = lgmp_getDMA(this, frame, dataSize);
    if (result->dmaFD < 0)
    {
      const LG_TransportStatus done = lgmp_doneFrameMessage(selected);
      DEBUG_ERROR("Failed to obtain a DMA buffer for the frame");
      return done == LG_TRANSPORT_DISCONNECTED ?
        done : LG_TRANSPORT_ERROR;
    }
  }

  if (!fullDamage && frame->damageRectsCount <= KVMFR_MAX_DAMAGE_RECTS)
  {
    result->damageRects      = frame->damageRects;
    result->damageRectsCount = frame->damageRectsCount;
  }
  else if (!fullDamage)
    DEBUG_WARN("Invalid damage rectangles, forcing a full update");

  this->framePending      = true;
  this->pendingFrame      = frame;
  this->pendingFrameQueue = selected->queue;
  return LG_TRANSPORT_OK;
}

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

static void lgmp_getFrameTiming(LG_Transport * this,
    const LG_TransportFrame * frame, LG_TransportFrameTiming * timing)
{
  memset(timing, 0, sizeof(*timing));
  if (!this->framePending || !this->pendingFrame ||
      this->pendingFrame->frameSerial != frame->serial)
    return;

  /* The producer writes these immediately after publishing FrameBuffer::wp.
   * nextFrame can observe the header earlier, so briefly observe the
   * publication tail after onFrame consumes the framebuffer without sleeping
   * the frame-acquisition thread. */
  for (unsigned i = 0;
       !lgmp_frameTimingReady(this->pendingFrame) &&
       i < LGMP_TIMING_SPIN_COUNT;
       ++i)
  {
  }

  if (!lgmp_frameTimingReady(this->pendingFrame))
    return;

  timing->valid           = true;
  timing->captureTime     = this->pendingFrame->captureTime;
  timing->postProcessTime = this->pendingFrame->postProcessTime;
  timing->copyTime        = this->pendingFrame->copyTime;
  timing->readyTime       = this->pendingFrame->readyTime;
}

static void lgmp_releaseFrame(LG_Transport * this, LG_TransportFrame * frame)
{
  if (this->framePending && this->pendingFrameQueue)
    lgmpClientMessageDone(this->pendingFrameQueue);
  this->framePending      = false;
  this->pendingFrame      = NULL;
  this->pendingFrameQueue = NULL;
  memset(frame, 0, sizeof(*frame));
}

static LG_TransportStatus lgmp_nextPointer(LG_Transport * this,
    LG_TransportPointer * result)
{
  if (!this->connected)
    return LG_TRANSPORT_DISCONNECTED;

  uint32_t pointerFlags  = 0;
  size_t   shapeSize     = 0;
  size_t   transformSize = 0;
  LG_TransportStatus status = LG_TRANSPORT_OK;
  if (!this->pointerQueue)
  {
    LG_LOCK(this->pointerLock);
    status = lgmp_subscribe(this->client, LGMP_Q_POINTER,
        &this->pointerQueue);
    LG_UNLOCK(this->pointerLock);
    if (status == LG_TRANSPORT_TIMEOUT)
      usleep(1000);
  }
  if (status == LG_TRANSPORT_OK)
  {
    LGMPMessage message;
    status = lgmp_process(this->pointerQueue, this->cursorPollInterval,
        &message);
    if (status == LG_TRANSPORT_OK)
    {
      if (message.size < sizeof(KVMFRCursor))
      {
        lgmpClientMessageDone(this->pointerQueue);
        status = LG_TRANSPORT_ERROR;
      }
      else
      {
        const KVMFRCursor * cursor = (const KVMFRCursor *)message.mem;
        shapeSize = message.udata & CURSOR_FLAG_SHAPE ?
          (size_t)cursor->height * cursor->pitch : 0;
        transformSize =
          message.udata & CURSOR_FLAG_COLOR_TRANSFORM ?
            sizeof(KVMFRColorTransform) : 0;
        if (shapeSize > message.size - sizeof(*cursor) ||
            transformSize > message.size - sizeof(*cursor) - shapeSize)
        {
          lgmpClientMessageDone(this->pointerQueue);
          status = LG_TRANSPORT_ERROR;
        }
        else
        {
          const size_t needed = sizeof(*cursor) + shapeSize + transformSize;
          if (needed > this->pointerDataSize)
          {
            void * data = realloc(this->pointerData, needed);
            if (!data)
              status = LG_TRANSPORT_ERROR;
            else
            {
              this->pointerData     = data;
              this->pointerDataSize = needed;
            }
          }
          if (status == LG_TRANSPORT_OK)
          {
            memcpy(this->pointerData, message.mem, needed);
            pointerFlags = (uint32_t)message.udata;
          }
          lgmpClientMessageDone(this->pointerQueue);
        }
      }
    }
  }
  if (status != LG_TRANSPORT_OK)
    return status;

  const KVMFRCursor * cursor = (const KVMFRCursor *)this->pointerData;
  memset(result, 0, sizeof(*result));
  if (pointerFlags & CURSOR_FLAG_POSITION)
    result->flags |= LG_TRANSPORT_POINTER_POSITION;
  if (pointerFlags & CURSOR_FLAG_VISIBLE)
    result->flags |= LG_TRANSPORT_POINTER_VISIBLE;
  if (pointerFlags & CURSOR_FLAG_SHAPE)
    result->flags |= LG_TRANSPORT_POINTER_SHAPE;
  if (pointerFlags & CURSOR_FLAG_COLOR_TRANSFORM)
    result->flags |= LG_TRANSPORT_POINTER_COLOR_TRANSFORM;
  if (pointerFlags & CURSOR_FLAG_VISIBLE_VALID)
    result->flags |= LG_TRANSPORT_POINTER_VISIBLE_VALID;
  result->x             = cursor->x;
  result->y             = cursor->y;
  result->type          = cursor->type;
  result->hx            = cursor->hx;
  result->hy            = cursor->hy;
  result->width         = cursor->width;
  result->height        = cursor->height;
  result->pitch         = cursor->pitch;
  result->sdrWhiteLevel = cursor->sdrWhiteLevel;
  result->shape         = (const uint8_t *)(cursor + 1);
  if (transformSize)
    result->colorTransform = (const LGColorTransform *)(result->shape +
        shapeSize);
  return LG_TRANSPORT_OK;
}

static void lgmp_releasePointer(LG_Transport * this,
    LG_TransportPointer * pointer)
{
  memset(pointer, 0, sizeof(*pointer));
}

static LG_TransportStatus lgmp_sendControl(LG_Transport * this,
    const LG_TransportControl * control, LG_TransportControlToken * token)
{
  uint8_t buffer[LGMP_MSGS_SIZE];
  uint32_t size;
  switch (control->type)
  {
    case LG_TRANSPORT_CONTROL_SET_CURSOR_POS:
    {
      const KVMFRSetCursorPos message = {
        .msg.type = KVMFR_MESSAGE_SETCURSORPOS,
        .x        = control->cursorPos.x,
        .y        = control->cursorPos.y,
      };
      memcpy(buffer, &message, sizeof(message));
      size = sizeof(message);
      break;
    }
    case LG_TRANSPORT_CONTROL_WINDOW_SIZE:
    {
      const KVMFRWindowSize message = {
        .msg.type = KVMFR_MESSAGE_WINDOWSIZE,
        .w        = control->windowSize.width,
        .h        = control->windowSize.height,
      };
      memcpy(buffer, &message, sizeof(message));
      size = sizeof(message);
      break;
    }
    case LG_TRANSPORT_CONTROL_FRAME_SCHEDULE:
    {
      const KVMFRFrameSchedule message = {
        .msg.type              = KVMFR_MESSAGE_FRAME_SCHEDULE,
        .clientID              = this->clientID,
        .generation            = control->frameSchedule.generation,
        .flags                 = control->frameSchedule.flags,
        .period                = control->frameSchedule.period,
        .targetSlack           = control->frameSchedule.targetSlack,
        .phaseError            = control->frameSchedule.phaseError,
        .feedbackFrameSerial   = control->frameSchedule.feedbackFrameSerial,
        .feedbackScheduleEpoch =
          control->frameSchedule.feedbackScheduleEpoch,
        .lease                 = control->frameSchedule.lease,
      };
      memcpy(buffer, &message, sizeof(message));
      size = sizeof(message);
      break;
    }
    default:
      return LG_TRANSPORT_ERROR;
  }

  LG_LOCK(this->pointerLock);
  if (!this->pointerQueue)
  {
    LG_UNLOCK(this->pointerLock);
    return LG_TRANSPORT_UNAVAILABLE;
  }
  uint32_t serial;
  LGMP_STATUS status = lgmpClientSendData(this->pointerQueue, buffer, size,
      &serial);
  LG_UNLOCK(this->pointerLock);
  if (status != LGMP_OK)
    return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED :
      LG_TRANSPORT_ERROR;
  *token = serial;
  return LG_TRANSPORT_OK;
}

static LG_TransportStatus lgmp_controlStatus(LG_Transport * this,
    LG_TransportControlToken token)
{
  LG_LOCK(this->pointerLock);
  if (!this->pointerQueue)
  {
    LG_UNLOCK(this->pointerLock);
    return LG_TRANSPORT_DISCONNECTED;
  }
  uint32_t serial;
  LGMP_STATUS status = lgmpClientGetSerial(this->pointerQueue, &serial);
  LG_UNLOCK(this->pointerLock);
  if (status != LGMP_OK)
    return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED :
      LG_TRANSPORT_ERROR;
  return serial >= token ? LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE;
}

const LG_TransportOps LGT_LGMP =
{
  .name           = "lgmp",
  .setup          = lgmp_setup,
  .create         = lgmp_create,
  .destroy        = lgmp_destroy,
  .connect        = lgmp_connect,
  .disconnect     = lgmp_disconnect,
  .sessionValid   = lgmp_sessionValid,
  .supportsDMA    = lgmp_supportsDMA,
  .attachRenderer = lgmp_attachRenderer,
  .detachRenderer = lgmp_detachRenderer,
  .nextFrame      = lgmp_nextFrame,
  .getFrameTiming = lgmp_getFrameTiming,
  .releaseFrame   = lgmp_releaseFrame,
  .stopFrame      = lgmp_stopFrame,
  .nextPointer    = lgmp_nextPointer,
  .releasePointer = lgmp_releasePointer,
  .stopPointer    = lgmp_stopPointer,
  .sendControl    = lgmp_sendControl,
  .controlStatus  = lgmp_controlStatus,
};
