/*
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: MIT
 */

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>

#include <xf86drm.h>

#include "virgl_context.h"
#include "virgl_util.h"
#include "virglrenderer.h"

#include "util/anon_file.h"
#include "util/hash_table.h"
#include "util/libsync.h"
#include "util/macros.h"
#include "util/os_file.h"
#include "util/u_atomic.h"
#include "util/u_thread.h"

#include "drm_fence.h"

#include "msm_drm.h"
#include "msm_proto.h"
#include "msm_renderer.h"

static unsigned nr_timelines;
static uint32_t uabi_version;

/**
 * A single context (from the PoV of the virtio-gpu protocol) maps to
 * a single drm device open.  Other drm/msm constructs (ie. submitqueue)
 * are opaque to the protocol.
 *
 * Typically each guest process will open a single virtio-gpu "context".
 * The single drm device open maps to an individual msm_gem_address_space
 * on the kernel side, providing GPU address space isolation between
 * guest processes.
 *
 * GEM buffer objects are tracked via one of two id's:
 *  - resource-id:  global, assigned by guest kernel
 *  - blob-id:      context specific, assigned by guest userspace
 *
 * The blob-id is used to link the bo created via MSM_CCMD_GEM_NEW and
 * the get_blob() cb.  It is unused in the case of a bo that is imported
 * from another context.  An object is added to the blob table in GEM_NEW
 * and removed in ctx->get_blob() (where it is added to resource_table).
 * By avoiding having an obj in both tables, we can safely free remaining
 * entries in either hashtable at context teardown.
 */
struct msm_context {
   struct virgl_context base;

   struct msm_shmem *shmem;
   uint8_t *rsp_mem;
   uint32_t rsp_mem_sz;

   struct msm_ccmd_rsp *current_rsp;

   int fd;

   struct hash_table *blob_table;
   struct hash_table *resource_table;

   /**
    * Maps submit-queue id to ring_idx
    */
   struct hash_table *sq_to_ring_idx_table;

   int eventfd;

   /**
    * Indexed by ring_idx-1, which is the same as the submitqueue priority+1.
    * On the kernel side, there is some drm_sched_entity per {drm_file, prio}
    * tuple, and the sched entity determines the fence timeline, ie. submits
    * against a single sched entity complete in fifo order.
    */
   struct drm_timeline timelines[];
};
DEFINE_CAST(virgl_context, msm_context)

#define valid_payload_len(req) ((req)->len <= ((req)->hdr.len - sizeof(*(req))))

static struct hash_entry *
table_search(struct hash_table *ht, uint32_t key)
{
   /* zero is not a valid key for u32_keys hashtable: */
   if (!key)
      return NULL;
   return _mesa_hash_table_search(ht, (void *)(uintptr_t)key);
}

static int
gem_info(struct msm_context *mctx, uint32_t handle, uint32_t param, uint64_t *val)
{
   struct drm_msm_gem_info args = {
      .handle = handle,
      .info = param,
      .value = *val,
   };
   int ret;

   ret = drmCommandWriteRead(mctx->fd, DRM_MSM_GEM_INFO, &args, sizeof(args));
   if (ret)
      return ret;

   *val = args.value;
   return 0;
}

static int
gem_close(int fd, uint32_t handle)
{
   struct drm_gem_close close_req = {
      .handle = handle,
   };
   return drmIoctl(fd, DRM_IOCTL_GEM_CLOSE, &close_req);
}

struct msm_object {
   uint32_t blob_id;
   uint32_t res_id;
   uint32_t handle;
   uint32_t flags;
   uint32_t size;
   bool exported   : 1;
   bool exportable : 1;
   struct virgl_resource *res;
   uint8_t *map;
};

static struct msm_object *
msm_object_create(uint32_t handle, uint32_t flags, uint32_t size)
{
   struct msm_object *obj = calloc(1, sizeof(*obj));

   if (!obj)
      return NULL;

   obj->handle = handle;
   obj->flags = flags;
   obj->size = size;

   return obj;
}

static bool
valid_blob_id(struct msm_context *mctx, uint32_t blob_id)
{
   /* must be non-zero: */
   if (blob_id == 0)
      return false;

   /* must not already be in-use: */
   if (table_search(mctx->blob_table, blob_id))
      return false;

   return true;
}

static void
msm_object_set_blob_id(struct msm_context *mctx, struct msm_object *obj, uint32_t blob_id)
{
   assert(valid_blob_id(mctx, blob_id));

   obj->blob_id = blob_id;
   _mesa_hash_table_insert(mctx->blob_table, (void *)(uintptr_t)obj->blob_id, obj);
}

static bool
valid_res_id(struct msm_context *mctx, uint32_t res_id)
{
   return !table_search(mctx->resource_table, res_id);
}

static void
msm_object_set_res_id(struct msm_context *mctx, struct msm_object *obj, uint32_t res_id)
{
   assert(valid_res_id(mctx, res_id));

   obj->res_id = res_id;
   _mesa_hash_table_insert(mctx->resource_table, (void *)(uintptr_t)obj->res_id, obj);
}

static void
msm_remove_object(struct msm_context *mctx, struct msm_object *obj)
{
   drm_dbg("obj=%p, blob_id=%u, res_id=%u", obj, obj->blob_id, obj->res_id);
   _mesa_hash_table_remove_key(mctx->resource_table, (void *)(uintptr_t)obj->res_id);
}

static struct msm_object *
msm_retrieve_object_from_blob_id(struct msm_context *mctx, uint64_t blob_id)
{
   assert((blob_id >> 32) == 0);
   uint32_t id = blob_id;
   struct hash_entry *entry = table_search(mctx->blob_table, id);
   if (!entry)
      return NULL;
   struct msm_object *obj = entry->data;
   _mesa_hash_table_remove(mctx->blob_table, entry);
   return obj;
}

static struct msm_object *
msm_get_object_from_res_id(struct msm_context *mctx, uint32_t res_id)
{
   const struct hash_entry *entry = table_search(mctx->resource_table, res_id);
   return likely(entry) ? entry->data : NULL;
}

static uint32_t
handle_from_res_id(struct msm_context *mctx, uint32_t res_id)
{
   struct msm_object *obj = msm_get_object_from_res_id(mctx, res_id);
   if (!obj)
      return 0;    /* zero is an invalid GEM handle */
   return obj->handle;
}

static bool
has_cached_coherent(int fd)
{
   struct drm_msm_gem_new new_req = {
      .size = 0x1000,
      .flags = MSM_BO_CACHED_COHERENT,
   };

   /* Do a test allocation to see if cached-coherent is supported: */
   if (!drmCommandWriteRead(fd, DRM_MSM_GEM_NEW, &new_req, sizeof(new_req))) {
      gem_close(fd, new_req.handle);
      return true;
   }

   return false;
}

static int
get_param64(int fd, uint32_t param, uint64_t *value)
{
   struct drm_msm_param req = {
      .pipe = MSM_PIPE_3D0,
      .param = param,
   };
   int ret;

   *value = 0;

   ret = drmCommandWriteRead(fd, DRM_MSM_GET_PARAM, &req, sizeof(req));
   if (ret)
      return ret;

   *value = req.value;

   return 0;
}

static int
get_param32(int fd, uint32_t param, uint32_t *value)
{
   uint64_t v64;
   int ret = get_param64(fd, param, &v64);
   *value = v64;
   return ret;
}

/**
 * Probe capset params.
 */
int
msm_renderer_probe(int fd, struct virgl_renderer_capset_drm *capset)
{
   drm_log("");

   /* Require MSM_SUBMIT_FENCE_SN_IN: */
   if (capset->version_minor < 9) {
      drm_log("Host kernel too old");
      return -ENOTSUP;
   }

   capset->wire_format_version = 2;
   capset->u.msm.has_cached_coherent = has_cached_coherent(fd);

   get_param32(fd, MSM_PARAM_PRIORITIES, &capset->u.msm.priorities);
   get_param64(fd, MSM_PARAM_VA_START,   &capset->u.msm.va_start);
   get_param64(fd, MSM_PARAM_VA_SIZE,    &capset->u.msm.va_size);
   get_param32(fd, MSM_PARAM_GPU_ID,     &capset->u.msm.gpu_id);
   get_param32(fd, MSM_PARAM_GMEM_SIZE,  &capset->u.msm.gmem_size);
   get_param64(fd, MSM_PARAM_GMEM_BASE,  &capset->u.msm.gmem_base);
   get_param64(fd, MSM_PARAM_CHIP_ID,    &capset->u.msm.chip_id);
   get_param32(fd, MSM_PARAM_MAX_FREQ,   &capset->u.msm.max_freq);

   nr_timelines = capset->u.msm.priorities;
   uabi_version = capset->version_minor;

   drm_log("wire_format_version: %u", capset->wire_format_version);
   drm_log("version_major:       %u", capset->version_major);
   drm_log("version_minor:       %u", capset->version_minor);
   drm_log("version_patchlevel:  %u", capset->version_patchlevel);
   drm_log("has_cached_coherent: %u", capset->u.msm.has_cached_coherent);
   drm_log("priorities:          %u", capset->u.msm.priorities);
   drm_log("va_start:            0x%0" PRIx64, capset->u.msm.va_start);
   drm_log("va_size:             0x%0" PRIx64, capset->u.msm.va_size);
   drm_log("gpu_id:              %u", capset->u.msm.gpu_id);
   drm_log("gmem_size:           %u", capset->u.msm.gmem_size);
   drm_log("gmem_base:           0x%0" PRIx64, capset->u.msm.gmem_base);
   drm_log("chip_id:             0x%0" PRIx64, capset->u.msm.chip_id);
   drm_log("max_freq:            %u", capset->u.msm.max_freq);

   if (!capset->u.msm.va_size) {
      drm_log("Host kernel does not support userspace allocated IOVA");
      return -ENOTSUP;
   }

   return 0;
}

static void
msm_renderer_unmap_blob(struct msm_context *mctx)
{
   if (!mctx->shmem)
      return;

   uint32_t blob_size = mctx->rsp_mem_sz + mctx->shmem->rsp_mem_offset;

   munmap(mctx->shmem, blob_size);

   mctx->shmem = NULL;
   mctx->rsp_mem = NULL;
   mctx->rsp_mem_sz = 0;
}

static void
resource_delete_fxn(struct hash_entry *entry)
{
   free((void *)entry->data);
}

static void
msm_renderer_destroy(struct virgl_context *vctx)
{
   struct msm_context *mctx = to_msm_context(vctx);

   for (unsigned i = 0; i < nr_timelines; i++)
      drm_timeline_fini(&mctx->timelines[i]);

   close(mctx->eventfd);

   msm_renderer_unmap_blob(mctx);

   _mesa_hash_table_destroy(mctx->resource_table, resource_delete_fxn);
   _mesa_hash_table_destroy(mctx->blob_table, resource_delete_fxn);
   _mesa_hash_table_destroy(mctx->sq_to_ring_idx_table, NULL);

   close(mctx->fd);
   free(mctx);
}

static void
msm_renderer_attach_resource(struct virgl_context *vctx, struct virgl_resource *res)
{
   struct msm_context *mctx = to_msm_context(vctx);
   struct msm_object *obj = msm_get_object_from_res_id(mctx, res->res_id);

   drm_dbg("obj=%p, res_id=%u", obj, res->res_id);

   if (!obj) {
      int fd;
      enum virgl_resource_fd_type fd_type = virgl_resource_export_fd(res, &fd);

      /* If importing a dmabuf resource created by another context (or
       * externally), then import it to create a gem obj handle in our
       * context:
       */
      if (fd_type == VIRGL_RESOURCE_FD_DMABUF) {
         uint32_t handle;
         int ret;

         ret = drmPrimeFDToHandle(mctx->fd, fd, &handle);
         if (ret) {
            drm_log("Could not import: %s", strerror(errno));
            close(fd);
            return;
         }

         /* lseek() to get bo size */
         int size = lseek(fd, 0, SEEK_END);
         if (size < 0)
            drm_log("lseek failed: %d (%s)", size, strerror(errno));
         close(fd);

         obj = msm_object_create(handle, 0, size);
         if (!obj)
            return;

         msm_object_set_res_id(mctx, obj, res->res_id);

         drm_dbg("obj=%p, res_id=%u, handle=%u", obj, obj->res_id, obj->handle);
      } else {
         if (fd_type != VIRGL_RESOURCE_FD_INVALID)
            close(fd);
         return;
      }
   }

   obj->res = res;
}

static void
msm_renderer_detach_resource(struct virgl_context *vctx, struct virgl_resource *res)
{
   struct msm_context *mctx = to_msm_context(vctx);
   struct msm_object *obj = msm_get_object_from_res_id(mctx, res->res_id);

   drm_dbg("obj=%p, res_id=%u", obj, res->res_id);

   if (!obj || (obj->res != res))
      return;

   if (res->fd_type == VIRGL_RESOURCE_FD_SHM) {
      msm_renderer_unmap_blob(mctx);

      /* shmem resources don't have an backing host GEM bo:, so bail now: */
      return;
   }

   msm_remove_object(mctx, obj);

   if (obj->map)
      munmap(obj->map, obj->size);

   gem_close(mctx->fd, obj->handle);

   free(obj);
}

static enum virgl_resource_fd_type
msm_renderer_export_opaque_handle(struct virgl_context *vctx, struct virgl_resource *res,
                                  int *out_fd)
{
   struct msm_context *mctx = to_msm_context(vctx);
   struct msm_object *obj = msm_get_object_from_res_id(mctx, res->res_id);
   int ret;

   drm_dbg("obj=%p, res_id=%u", obj, res->res_id);

   if (!obj) {
      drm_log("invalid res_id %u", res->res_id);
      return VIRGL_RESOURCE_FD_INVALID;
   }

   if (!obj->exportable) {
      /* crosvm seems to like to export things it doesn't actually need an
       * fd for.. don't let it spam our fd table!
       */
      return VIRGL_RESOURCE_FD_INVALID;
   }

   ret = drmPrimeHandleToFD(mctx->fd, obj->handle, DRM_CLOEXEC | DRM_RDWR, out_fd);
   if (ret) {
      drm_log("failed to get dmabuf fd: %s", strerror(errno));
      return VIRGL_RESOURCE_FD_INVALID;
   }

   return VIRGL_RESOURCE_FD_DMABUF;
}

static int
msm_renderer_transfer_3d(UNUSED struct virgl_context *vctx,
                         UNUSED struct virgl_resource *res,
                         UNUSED const struct vrend_transfer_info *info,
                         UNUSED int transfer_mode)
{
   drm_log("unsupported");
   return -1;
}

static int
msm_renderer_get_blob(struct virgl_context *vctx, uint32_t res_id, uint64_t blob_id,
                      uint64_t blob_size, uint32_t blob_flags,
                      struct virgl_context_blob *blob)
{
   struct msm_context *mctx = to_msm_context(vctx);

   drm_dbg("blob_id=%" PRIu64 ", res_id=%u, blob_size=%" PRIu64 ", blob_flags=0x%x",
           blob_id, res_id, blob_size, blob_flags);

   if ((blob_id >> 32) != 0) {
      drm_log("invalid blob_id: %" PRIu64, blob_id);
      return -EINVAL;
   }

   /* blob_id of zero is reserved for the shmem buffer: */
   if (blob_id == 0) {
      int fd;

      if (blob_flags != VIRGL_RENDERER_BLOB_FLAG_USE_MAPPABLE) {
         drm_log("invalid blob_flags: 0x%x", blob_flags);
         return -EINVAL;
      }

      if (mctx->shmem) {
         drm_log("There can be only one!");
         return -EINVAL;
      }

      if (blob_size < sizeof(*mctx->shmem)) {
         drm_log("Invalid blob size");
         return -EINVAL;
      }

      fd = os_create_anonymous_file(blob_size, "msm-shmem");
      if (fd < 0) {
         drm_log("Failed to create shmem file: %s", strerror(errno));
         return -ENOMEM;
      }

      int ret = fcntl(fd, F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW);
      if (ret) {
         drm_log("fcntl failed: %s", strerror(errno));
         close(fd);
         return -ENOMEM;
      }

      mctx->shmem = mmap(NULL, blob_size, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
      if (mctx->shmem == MAP_FAILED) {
         drm_log("shmem mmap failed: %s", strerror(errno));
         close(fd);
         return -ENOMEM;
      }

      mctx->shmem->rsp_mem_offset = sizeof(*mctx->shmem);

      uint8_t *ptr = (uint8_t *)mctx->shmem;
      mctx->rsp_mem = &ptr[mctx->shmem->rsp_mem_offset];
      mctx->rsp_mem_sz = blob_size - mctx->shmem->rsp_mem_offset;

      blob->type = VIRGL_RESOURCE_FD_SHM;
      blob->u.fd = fd;
      blob->map_info = VIRGL_RENDERER_MAP_CACHE_CACHED;

      return 0;
   }

   if (!valid_res_id(mctx, res_id)) {
      drm_log("Invalid res_id %u", res_id);
      return -EINVAL;
   }

   struct msm_object *obj = msm_retrieve_object_from_blob_id(mctx, blob_id);

   /* If GEM_NEW fails, we can end up here without a backing obj: */
   if (!obj) {
      drm_log("No object");
      return -ENOENT;
   }

   /* a memory can only be exported once; we don't want two resources to point
    * to the same storage.
    */
   if (obj->exported) {
      drm_log("Already exported!");
      return -EINVAL;
   }

   /* The size we get from guest userspace is not necessarily rounded up to the
    * nearest page size, but the actual GEM buffer allocation is, as is the
    * guest GEM buffer (and therefore the blob_size value we get from the guest
    * kernel).
    */
   if (ALIGN_POT(obj->size, getpagesize()) != blob_size) {
      drm_log("Invalid blob size");
      return -EINVAL;
   }

   msm_object_set_res_id(mctx, obj, res_id);

   if (blob_flags & VIRGL_RENDERER_BLOB_FLAG_USE_SHAREABLE) {
      int fd, ret;

      ret = drmPrimeHandleToFD(mctx->fd, obj->handle, DRM_CLOEXEC | DRM_RDWR, &fd);
      if (ret) {
         drm_log("Export to fd failed");
         return -EINVAL;
      }

      blob->type = VIRGL_RESOURCE_FD_DMABUF;
      blob->u.fd = fd;
   } else {
      blob->type = VIRGL_RESOURCE_OPAQUE_HANDLE;
      blob->u.opaque_handle = obj->handle;
   }

   if (obj->flags & MSM_BO_CACHED_COHERENT) {
      blob->map_info = VIRGL_RENDERER_MAP_CACHE_CACHED;
   } else {
      blob->map_info = VIRGL_RENDERER_MAP_CACHE_WC;
   }

   obj->exported = true;
   obj->exportable = !!(blob_flags & VIRGL_RENDERER_BLOB_FLAG_USE_MAPPABLE);

   return 0;
}

static void *
msm_context_rsp_noshadow(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   return &mctx->rsp_mem[hdr->rsp_off];
}

static void *
msm_context_rsp(struct msm_context *mctx, const struct msm_ccmd_req *hdr, unsigned len)
{
   unsigned rsp_mem_sz = mctx->rsp_mem_sz;
   unsigned off = hdr->rsp_off;

   if ((off > rsp_mem_sz) || (len > rsp_mem_sz - off)) {
      drm_log("invalid shm offset: off=%u, len=%u (shmem_size=%u)", off, len, rsp_mem_sz);
      return NULL;
   }

   struct msm_ccmd_rsp *rsp = msm_context_rsp_noshadow(mctx, hdr);

   assert(len >= sizeof(*rsp));

   /* With newer host and older guest, we could end up wanting a larger rsp struct
    * than guest expects, so allocate a shadow buffer in this case rather than
    * having to deal with this in all the different ccmd handlers.  This is similar
    * in a way to what drm_ioctl() does.
    */
   if (len > rsp->len) {
      rsp = malloc(len);
      if (!rsp)
         return NULL;
      rsp->len = len;
   }

   mctx->current_rsp = rsp;

   return rsp;
}

static int
msm_ccmd_nop(UNUSED struct msm_context *mctx, UNUSED const struct msm_ccmd_req *hdr)
{
   return 0;
}

static int
msm_ccmd_ioctl_simple(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_ioctl_simple_req *req = to_msm_ccmd_ioctl_simple_req(hdr);
   unsigned payload_len = _IOC_SIZE(req->cmd);
   unsigned req_len = size_add(sizeof(*req), payload_len);

   if (hdr->len != req_len) {
      drm_log("%u != %u", hdr->len, req_len);
      return -EINVAL;
   }

   /* Apply a reasonable upper bound on ioctl size: */
   if (payload_len > 128) {
      drm_log("invalid ioctl payload length: %u", payload_len);
      return -EINVAL;
   }

   /* Allow-list of supported ioctls: */
   unsigned iocnr = _IOC_NR(req->cmd) - DRM_COMMAND_BASE;
   switch (iocnr) {
   case DRM_MSM_GET_PARAM:
   case DRM_MSM_SUBMITQUEUE_NEW:
   case DRM_MSM_SUBMITQUEUE_CLOSE:
      break;
   default:
      drm_log("invalid ioctl: %08x (%u)", req->cmd, iocnr);
      return -EINVAL;
   }

   struct msm_ccmd_ioctl_simple_rsp *rsp;
   unsigned rsp_len = sizeof(*rsp);

   if (req->cmd & IOC_OUT)
      rsp_len = size_add(rsp_len, payload_len);

   rsp = msm_context_rsp(mctx, hdr, rsp_len);

   if (!rsp)
      return -ENOMEM;

   /* Copy the payload because the kernel can write (if IOC_OUT bit
    * is set) and to avoid casting away the const:
    */
   char payload[payload_len];
   memcpy(payload, req->payload, payload_len);

   rsp->ret = drmIoctl(mctx->fd, req->cmd, payload);

   if (req->cmd & IOC_OUT)
      memcpy(rsp->payload, payload, payload_len);

   if (iocnr == DRM_MSM_SUBMITQUEUE_NEW && !rsp->ret) {
      struct drm_msm_submitqueue *args = (void *)payload;

      drm_dbg("submitqueue %u, prio %u", args->id, args->prio);

      _mesa_hash_table_insert(mctx->sq_to_ring_idx_table, (void *)(uintptr_t)args->id,
                              (void *)(uintptr_t)args->prio);
   }

   return 0;
}

static int
msm_ccmd_gem_new(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_new_req *req = to_msm_ccmd_gem_new_req(hdr);
   int ret = 0;

   if (!valid_blob_id(mctx, req->blob_id)) {
      drm_log("Invalid blob_id %u", req->blob_id);
      ret = -EINVAL;
      goto out_error;
   }

   /*
    * First part, allocate the GEM bo:
    */
   struct drm_msm_gem_new gem_new = {
      .size = req->size,
      .flags = req->flags,
   };

   ret = drmCommandWriteRead(mctx->fd, DRM_MSM_GEM_NEW, &gem_new, sizeof(gem_new));
   if (ret) {
      drm_log("GEM_NEW failed: %d (%s)", ret, strerror(errno));
      goto out_error;
   }

   /*
    * Second part, set the iova:
    */
   uint64_t iova = req->iova;
   ret = gem_info(mctx, gem_new.handle, MSM_INFO_SET_IOVA, &iova);
   if (ret) {
      drm_log("SET_IOVA failed: %d (%s)", ret, strerror(errno));
      goto out_close;
   }

   /*
    * And then finally create our msm_object for tracking the resource,
    * and add to blob table:
    */
   struct msm_object *obj = msm_object_create(gem_new.handle, req->flags, req->size);

   if (!obj) {
      ret = -ENOMEM;
      goto out_close;
   }

   msm_object_set_blob_id(mctx, obj, req->blob_id);

   drm_dbg("obj=%p, blob_id=%u, handle=%u, iova=%" PRIx64, obj, obj->blob_id,
           obj->handle, iova);

   return 0;

out_close:
   gem_close(mctx->fd, gem_new.handle);
out_error:
   if (mctx->shmem)
      mctx->shmem->async_error++;
   return ret;
}

static int
msm_ccmd_gem_set_iova(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_set_iova_req *req = to_msm_ccmd_gem_set_iova_req(hdr);
   struct msm_object *obj = msm_get_object_from_res_id(mctx, req->res_id);
   int ret = 0;

   if (!obj) {
      drm_log("Could not lookup obj: res_id=%u", req->res_id);
      ret = -ENOENT;
      goto out_error;
   }

   uint64_t iova = req->iova;
   if (iova) {
      TRACE_SCOPE("SET_IOVA");
      ret = gem_info(mctx, obj->handle, MSM_INFO_SET_IOVA, &iova);
   } else {
      TRACE_SCOPE("CLEAR_IOVA");
      ret = gem_info(mctx, obj->handle, MSM_INFO_SET_IOVA, &iova);
   }
   if (ret) {
      drm_log("SET_IOVA failed: %d (%s)", ret, strerror(errno));
      goto out_error;
   }

   drm_dbg("obj=%p, blob_id=%u, handle=%u, iova=%" PRIx64, obj, obj->blob_id,
           obj->handle, iova);

   return 0;

out_error:
   if (mctx->shmem)
      mctx->shmem->async_error++;
   return 0;
}

static int
msm_ccmd_gem_cpu_prep(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_cpu_prep_req *req = to_msm_ccmd_gem_cpu_prep_req(hdr);
   struct msm_ccmd_gem_cpu_prep_rsp *rsp = msm_context_rsp(mctx, hdr, sizeof(*rsp));

   if (!rsp)
      return -ENOMEM;

   struct drm_msm_gem_cpu_prep args = {
      .handle = handle_from_res_id(mctx, req->res_id),
      .op = req->op | MSM_PREP_NOSYNC,
   };

   if (uabi_version >= 11)
      args.op |= MSM_PREP_BOOST;

   rsp->ret = drmCommandWrite(mctx->fd, DRM_MSM_GEM_CPU_PREP, &args, sizeof(args));

   return 0;
}

static int
msm_ccmd_gem_set_name(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_set_name_req *req = to_msm_ccmd_gem_set_name_req(hdr);

   struct drm_msm_gem_info args = {
      .handle = handle_from_res_id(mctx, req->res_id),
      .info = MSM_INFO_SET_NAME,
      .value = VOID2U64(req->payload),
      .len = req->len,
   };

   if (!valid_payload_len(req))
      return -EINVAL;

   int ret = drmCommandWrite(mctx->fd, DRM_MSM_GEM_INFO, &args, sizeof(args));
   if (ret)
      drm_log("ret=%d, len=%u, name=%.*s", ret, req->len, req->len, req->payload);

   return 0;
}

static void
msm_dump_submit(struct drm_msm_gem_submit *req)
{
#ifndef NDEBUG
   drm_log("  flags=0x%x, queueid=%u", req->flags, req->queueid);
   for (unsigned i = 0; i < req->nr_bos; i++) {
      struct drm_msm_gem_submit_bo *bos = U642VOID(req->bos);
      struct drm_msm_gem_submit_bo *bo = &bos[i];
      drm_log("  bos[%d]: handle=%u, flags=%x", i, bo->handle, bo->flags);
   }
   for (unsigned i = 0; i < req->nr_cmds; i++) {
      struct drm_msm_gem_submit_cmd *cmds = U642VOID(req->cmds);
      struct drm_msm_gem_submit_cmd *cmd = &cmds[i];
      drm_log("  cmd[%d]: type=%u, submit_idx=%u, submit_offset=%u, size=%u", i,
              cmd->type, cmd->submit_idx, cmd->submit_offset, cmd->size);
   }
#else
   (void)req;
#endif
}

static int
msm_ccmd_gem_submit(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_submit_req *req = to_msm_ccmd_gem_submit_req(hdr);

   size_t sz = sizeof(*req);
   sz = size_add(sz, size_mul(req->nr_bos,  sizeof(struct drm_msm_gem_submit_bo)));
   sz = size_add(sz, size_mul(req->nr_cmds, sizeof(struct drm_msm_gem_submit_cmd)));

   /* Normally kernel would validate out of bounds situations and return -EFAULT,
    * but since we are copying the bo handles, we need to validate that the
    * guest can't trigger us to make an out of bounds memory access:
    */
   if (sz > hdr->len) {
      drm_log("out of bounds: nr_bos=%u, nr_cmds=%u", req->nr_bos, req->nr_cmds);
      return -ENOSPC;
   }

   const unsigned bo_limit = 8192 / sizeof(struct drm_msm_gem_submit_bo);
   bool bos_on_stack = req->nr_bos < bo_limit;
   struct drm_msm_gem_submit_bo _bos[bos_on_stack ? req->nr_bos : 0];
   struct drm_msm_gem_submit_bo *bos;

   if (bos_on_stack) {
      bos = _bos;
   } else {
      bos = malloc(req->nr_bos * sizeof(bos[0]));
      if (!bos)
         return -ENOMEM;
   }

   memcpy(bos, req->payload, req->nr_bos * sizeof(bos[0]));

   for (uint32_t i = 0; i < req->nr_bos; i++)
      bos[i].handle = handle_from_res_id(mctx, bos[i].handle);

   uint32_t fence_flags = MSM_SUBMIT_FENCE_FD_OUT | MSM_SUBMIT_FENCE_SN_IN;

   int in_fence_fd = virgl_context_take_in_fence_fd(&mctx->base);
   if (in_fence_fd >= 0)
         fence_flags |= MSM_SUBMIT_FENCE_FD_IN;

   struct drm_msm_gem_submit args = {
      .flags = req->flags | fence_flags,
      .fence = req->fence,
      .fence_fd = in_fence_fd,
      .nr_bos = req->nr_bos,
      .nr_cmds = req->nr_cmds,
      .bos = VOID2U64(bos),
      .cmds = VOID2U64(&req->payload[req->nr_bos * sizeof(struct drm_msm_gem_submit_bo)]),
      .queueid = req->queue_id,
   };

   close(in_fence_fd);

   int ret = drmCommandWriteRead(mctx->fd, DRM_MSM_GEM_SUBMIT, &args, sizeof(args));
   drm_dbg("fence=%u, ret=%d", args.fence, ret);

   if (unlikely(ret)) {
      drm_log("submit failed: %s", strerror(errno));
      msm_dump_submit(&args);
      if (mctx->shmem)
         mctx->shmem->async_error++;
   } else {
      const struct hash_entry *entry =
            table_search(mctx->sq_to_ring_idx_table, args.queueid);

      if (!entry) {
         drm_log("unknown submitqueue: %u", args.queueid);
         goto out;
      }

      unsigned prio = (uintptr_t)entry->data;

      drm_timeline_set_last_fence_fd(&mctx->timelines[prio], args.fence_fd);
   }

out:
   if (!bos_on_stack)
      free(bos);
   return 0;
}

static int
map_object(struct msm_context *mctx, struct msm_object *obj)
{
   uint64_t offset = 0;
   int ret;

   if (obj->map)
      return 0;

   uint32_t handle = handle_from_res_id(mctx, obj->res_id);
   ret = gem_info(mctx, handle, MSM_INFO_GET_OFFSET, &offset);
   if (ret) {
      drm_log("alloc failed: %s", strerror(errno));
      return ret;
   }

   uint8_t *map =
      mmap(0, obj->size, PROT_READ | PROT_WRITE, MAP_SHARED, mctx->fd, offset);
   if (map == MAP_FAILED) {
      drm_log("mmap failed: %s", strerror(errno));
      return -ENOMEM;
   }

   obj->map = map;

   return 0;
}

static int
msm_ccmd_gem_upload(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_gem_upload_req *req = to_msm_ccmd_gem_upload_req(hdr);
   int ret;

   if (req->pad || !valid_payload_len(req)) {
      drm_log("Invalid upload ccmd");
      return -EINVAL;
   }

   struct msm_object *obj = msm_get_object_from_res_id(mctx, req->res_id);
   if (!obj) {
      drm_log("No obj: res_id=%u", req->res_id);
      return -ENOENT;
   }

   ret = map_object(mctx, obj);
   if (ret)
      return ret;

   memcpy(&obj->map[req->off], req->payload, req->len);

   return 0;
}

static int
msm_ccmd_submitqueue_query(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_submitqueue_query_req *req =
      to_msm_ccmd_submitqueue_query_req(hdr);
   struct msm_ccmd_submitqueue_query_rsp *rsp =
      msm_context_rsp(mctx, hdr, size_add(sizeof(*rsp), req->len));

   if (!rsp)
      return -ENOMEM;

   struct drm_msm_submitqueue_query args = {
      .data = VOID2U64(rsp->payload),
      .id = req->queue_id,
      .param = req->param,
      .len = req->len,
   };

   rsp->ret =
      drmCommandWriteRead(mctx->fd, DRM_MSM_SUBMITQUEUE_QUERY, &args, sizeof(args));

   rsp->out_len = args.len;

   return 0;
}

static int
msm_ccmd_wait_fence(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_wait_fence_req *req = to_msm_ccmd_wait_fence_req(hdr);
   struct msm_ccmd_wait_fence_rsp *rsp = msm_context_rsp(mctx, hdr, sizeof(*rsp));

   if (!rsp)
      return -ENOMEM;

   struct timespec t;

   /* Use current time as timeout, to avoid blocking: */
   clock_gettime(CLOCK_MONOTONIC, &t);

   struct drm_msm_wait_fence args = {
      .fence = req->fence,
      .flags = (uabi_version) >= 11 ? MSM_WAIT_FENCE_BOOST : 0,
      .queueid = req->queue_id,
      .timeout =
         {
            .tv_sec = t.tv_sec,
            .tv_nsec = t.tv_nsec,
         },
   };

   rsp->ret = drmCommandWrite(mctx->fd, DRM_MSM_WAIT_FENCE, &args, sizeof(args));

   return 0;
}

static int
msm_ccmd_set_debuginfo(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   const struct msm_ccmd_set_debuginfo_req *req = to_msm_ccmd_set_debuginfo_req(hdr);

   size_t sz = sizeof(*req);
   sz = size_add(sz, req->comm_len);
   sz = size_add(sz, req->cmdline_len);

   if (sz > hdr->len) {
      drm_log("out of bounds: comm_len=%u, cmdline_len=%u", req->comm_len, req->cmdline_len);
      return -ENOSPC;
   }

   struct drm_msm_param set_comm = {
      .pipe = MSM_PIPE_3D0,
      .param = MSM_PARAM_COMM,
      .value = VOID2U64(&req->payload[0]),
      .len = req->comm_len,
   };

   drmCommandWriteRead(mctx->fd, DRM_MSM_SET_PARAM, &set_comm, sizeof(set_comm));

   struct drm_msm_param set_cmdline = {
      .pipe = MSM_PIPE_3D0,
      .param = MSM_PARAM_CMDLINE,
      .value = VOID2U64(&req->payload[req->comm_len]),
      .len = req->cmdline_len,
   };

   drmCommandWriteRead(mctx->fd, DRM_MSM_SET_PARAM, &set_cmdline, sizeof(set_cmdline));

   return 0;
}

static const struct ccmd {
   const char *name;
   int (*handler)(struct msm_context *mctx, const struct msm_ccmd_req *hdr);
   size_t size;
} ccmd_dispatch[] = {
#define HANDLER(N, n)                                                                    \
   [MSM_CCMD_##N] = {#N, msm_ccmd_##n, sizeof(struct msm_ccmd_##n##_req)}
   HANDLER(NOP, nop),
   HANDLER(IOCTL_SIMPLE, ioctl_simple),
   HANDLER(GEM_NEW, gem_new),
   HANDLER(GEM_SET_IOVA, gem_set_iova),
   HANDLER(GEM_CPU_PREP, gem_cpu_prep),
   HANDLER(GEM_SET_NAME, gem_set_name),
   HANDLER(GEM_SUBMIT, gem_submit),
   HANDLER(GEM_UPLOAD, gem_upload),
   HANDLER(SUBMITQUEUE_QUERY, submitqueue_query),
   HANDLER(WAIT_FENCE, wait_fence),
   HANDLER(SET_DEBUGINFO, set_debuginfo),
};

static int
submit_cmd_dispatch(struct msm_context *mctx, const struct msm_ccmd_req *hdr)
{
   int ret;

   if (hdr->cmd >= ARRAY_SIZE(ccmd_dispatch)) {
      drm_log("invalid cmd: %u", hdr->cmd);
      return -EINVAL;
   }

   const struct ccmd *ccmd = &ccmd_dispatch[hdr->cmd];

   if (!ccmd->handler) {
      drm_log("no handler: %u", hdr->cmd);
      return -EINVAL;
   }

   drm_dbg("%s: hdr={cmd=%u, len=%u, seqno=%u, rsp_off=0x%x)", ccmd->name, hdr->cmd,
           hdr->len, hdr->seqno, hdr->rsp_off);

   void *trace_scope = TRACE_SCOPE_BEGIN(ccmd->name);

   /* If the request length from the guest is smaller than the expected
    * size, ie. newer host and older guest, we need to make a copy of
    * the request with the new fields at the end zero initialized.
    */
   if (ccmd->size > hdr->len) {
      uint8_t buf[ccmd->size];

      memcpy(&buf[0], hdr, hdr->len);
      memset(&buf[hdr->len], 0, ccmd->size - hdr->len);

      ret = ccmd->handler(mctx, (struct msm_ccmd_req *)buf);
   } else {
      ret = ccmd->handler(mctx, hdr);
   }

   TRACE_SCOPE_END(trace_scope);

   if (ret) {
      drm_log("%s: dispatch failed: %d (%s)", ccmd->name, ret, strerror(errno));
      return ret;
   }

   /* If the response length from the guest is smaller than the
    * expected size, ie. newer host and older guest, then a shadow
    * copy is used, and we need to copy back to the actual rsp
    * buffer.
    */
   struct msm_ccmd_rsp *rsp = msm_context_rsp_noshadow(mctx, hdr);
   if (mctx->current_rsp && (mctx->current_rsp != rsp)) {
      unsigned len = rsp->len;
      memcpy(rsp, mctx->current_rsp, len);
      rsp->len = len;
      free(mctx->current_rsp);
   }
   mctx->current_rsp = NULL;

   /* Note that commands with no response, like SET_DEBUGINFO, could
    * be sent before the shmem buffer is allocated:
    */
   if (mctx->shmem) {
      /* TODO better way to do this?  We need ACQ_REL semanatics (AFAIU)
       * to ensure that writes to response buffer are visible to the
       * guest process before the update of the seqno.  Otherwise we
       * could just use p_atomic_set.
       */
      uint32_t seqno = hdr->seqno;
      p_atomic_xchg(&mctx->shmem->seqno, seqno);
   }

   return 0;
}

static int
msm_renderer_submit_cmd(struct virgl_context *vctx, const void *_buffer, size_t size)
{
   struct msm_context *mctx = to_msm_context(vctx);
   const uint8_t *buffer = _buffer;

   while (size >= sizeof(struct msm_ccmd_req)) {
      const struct msm_ccmd_req *hdr = (const struct msm_ccmd_req *)buffer;

      /* Sanity check first: */
      if ((hdr->len > size) || (hdr->len < sizeof(*hdr)) || (hdr->len % 4)) {
         drm_log("bad size, %u vs %zu (%u)", hdr->len, size, hdr->cmd);
         return -EINVAL;
      }

      if (hdr->rsp_off % 4) {
         drm_log("bad rsp_off, %u", hdr->rsp_off);
         return -EINVAL;
      }

      int ret = submit_cmd_dispatch(mctx, hdr);
      if (ret) {
         drm_log("dispatch failed: %d (%u)", ret, hdr->cmd);
         return ret;
      }

      buffer += hdr->len;
      size -= hdr->len;
   }

   if (size > 0) {
      drm_log("bad size, %zu trailing bytes", size);
      return -EINVAL;
   }

   return 0;
}

static int
msm_renderer_get_fencing_fd(struct virgl_context *vctx)
{
   struct msm_context *mctx = to_msm_context(vctx);
   return mctx->eventfd;
}

static void
msm_renderer_retire_fences(UNUSED struct virgl_context *vctx)
{
   /* No-op as VIRGL_RENDERER_ASYNC_FENCE_CB is required */
}

static void
msm_renderer_fence_retire(struct virgl_context *vctx,
                          uint32_t ring_idx,
                          uint64_t fence_id)
{
   struct msm_context *mctx = to_msm_context(vctx);

   get_param32(mctx->fd, MSM_PARAM_FAULTS, &mctx->shmem->global_faults);

   vctx->fence_retire(vctx, ring_idx, fence_id);
}

static int
msm_renderer_submit_fence(struct virgl_context *vctx, uint32_t flags, uint32_t ring_idx,
                          uint64_t fence_id)
{
   struct msm_context *mctx = to_msm_context(vctx);

   drm_dbg("flags=0x%x, ring_idx=%" PRIu32 ", fence_id=%" PRIu64, flags,
           ring_idx, fence_id);

   /* timeline is ring_idx-1 (because ring_idx 0 is host CPU timeline) */
   if (ring_idx > nr_timelines) {
      drm_log("invalid ring_idx: %" PRIu32, ring_idx);
      return -EINVAL;
   }

   /* ring_idx zero is used for the guest to synchronize with host CPU,
    * meaning by the time ->submit_fence() is called, the fence has
    * already passed.. so just immediate signal:
    */
   if (ring_idx == 0) {
      vctx->fence_retire(vctx, ring_idx, fence_id);
      return 0;
   }

   return drm_timeline_submit_fence(&mctx->timelines[ring_idx - 1], flags, fence_id);
}

struct virgl_context *
msm_renderer_create(int fd)
{
   struct msm_context *mctx;

   drm_log("");

   mctx = calloc(1, sizeof(*mctx) + (nr_timelines * sizeof(mctx->timelines[0])));
   if (!mctx)
      return NULL;

   mctx->fd = fd;

   /* Indexed by blob_id, but only lower 32b of blob_id are used: */
   mctx->blob_table = _mesa_hash_table_create_u32_keys(NULL);
   /* Indexed by res_id: */
   mctx->resource_table = _mesa_hash_table_create_u32_keys(NULL);
   /* Indexed by submitqueue-id: */
   mctx->sq_to_ring_idx_table = _mesa_hash_table_create_u32_keys(NULL);

   mctx->eventfd = create_eventfd(0);

   for (unsigned i = 0; i < nr_timelines; i++) {
      unsigned ring_idx = i + 1; /* ring_idx 0 is host CPU */
      drm_timeline_init(&mctx->timelines[i], &mctx->base, "msm-sync", mctx->eventfd,
                        ring_idx, msm_renderer_fence_retire);
   }

   mctx->base.destroy = msm_renderer_destroy;
   mctx->base.attach_resource = msm_renderer_attach_resource;
   mctx->base.detach_resource = msm_renderer_detach_resource;
   mctx->base.export_opaque_handle = msm_renderer_export_opaque_handle;
   mctx->base.transfer_3d = msm_renderer_transfer_3d;
   mctx->base.get_blob = msm_renderer_get_blob;
   mctx->base.submit_cmd = msm_renderer_submit_cmd;
   mctx->base.get_fencing_fd = msm_renderer_get_fencing_fd;
   mctx->base.retire_fences = msm_renderer_retire_fences;
   mctx->base.submit_fence = msm_renderer_submit_fence;
   mctx->base.supports_fence_sharing = true;

   return &mctx->base;
}
