/*
 * Copyright 2024 Advanced Micro Devices, Inc.
 *
 * SPDX-License-Identifier: MIT
 */

#include "amdgpu_virtio_private.h"

#include "util/bitscan.h"
#include "util/log.h"
#include "util/os_file.h"
#include "util/u_debug.h"
#include "util/u_sync_provider.h"

#include <xf86drm.h>

/* amdvgpu_device manage the virtual GPU.
 *
 * It owns a vdrm_device instance, the rings and manage seqno.
 * Since it's a drop-in replacement for libdrm_amdgpu's amdgpu_device,
 * it follows its behavior: if the same device is opened multiple times,
 * the same amdvgpu_device will be used.
 */
static simple_mtx_t dev_mutex = SIMPLE_MTX_INITIALIZER;
static amdvgpu_device_handle dev_list;

static int fd_compare(int fd1, int fd2)
{
   char *name1 = drmGetPrimaryDeviceNameFromFd(fd1);
   char *name2 = drmGetPrimaryDeviceNameFromFd(fd2);
   int result;

   if (name1 == NULL || name2 == NULL) {
      free(name1);
      free(name2);
      return 0;
   }

   result = strcmp(name1, name2);
   free(name1);
   free(name2);

   return result;
}

static void amdvgpu_device_reference(struct amdvgpu_device **dst,
                                     struct amdvgpu_device *src)
{
   if (update_references(*dst ? &(*dst)->refcount : NULL,
                         src ? &src->refcount : NULL)) {
      struct amdvgpu_device *dev, *prev = NULL;
      for (dev = dev_list; dev; dev = dev->next) {
         if (dev == (*dst)) {
            if (prev == NULL)
               dev_list = dev->next;
            else
               prev->next = dev->next;
            break;
         }
         prev = dev;
      }

      dev = *dst;

      /* Destroy BOs before closing vdrm */
      hash_table_u64_foreach(dev->handle_to_vbo, entry) {
         struct amdvgpu_bo *bo = entry.data;
         amdvgpu_bo_free(dev, bo);
      }
      _mesa_hash_table_u64_destroy(dev->handle_to_vbo);
      /* Destroy contextx. */
      hash_table_foreach(&dev->contexts, entry)
         amdvgpu_cs_ctx_free(dev, (uint32_t)(uintptr_t)entry->key);
      _mesa_hash_table_clear(&dev->contexts, NULL);

      simple_mtx_destroy(&dev->handle_to_vbo_mutex);
      simple_mtx_destroy(&dev->contexts_mutex);

      amdgpu_va_manager_deinit(dev->va_mgr);

      vdrm_device_close(dev->vdev);

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

   *dst = src;
}

int amdvgpu_device_deinitialize(amdvgpu_device_handle dev) {
   simple_mtx_lock(&dev_mutex);
   amdvgpu_device_reference(&dev, NULL);
   simple_mtx_unlock(&dev_mutex);
   return 0;
}

int amdvgpu_device_initialize(int fd, uint32_t *drm_major, uint32_t *drm_minor,
                              amdvgpu_device_handle* dev_out,
                              struct util_sync_provider **p) {
   simple_mtx_lock(&dev_mutex);
   amdvgpu_device_handle dev;

   for (dev = dev_list; dev; dev = dev->next)
      if (fd_compare(dev->fd, fd) == 0)
         break;

   if (dev) {
      *dev_out = NULL;
      amdvgpu_device_reference(dev_out, dev);
      *drm_major = dev->vdev->caps.version_major;
      *drm_minor = dev->vdev->caps.version_minor;
      simple_mtx_unlock(&dev_mutex);
      goto init_sync_provider;
   }

   /* fd is owned by the amdgpu_screen_winsys that called this function.
    * amdgpu_screen_winsys' lifetime may be shorter than the device's one,
    * so dup fd to tie its lifetime to the device's one.
    */
   fd = os_dupfd_cloexec(fd);

   struct vdrm_device *vdev = vdrm_device_connect(fd, VIRTGPU_DRM_CONTEXT_AMDGPU);
   if (vdev == NULL) {
      mesa_loge("vdrm_device_connect failed\n");
      simple_mtx_unlock(&dev_mutex);
      return -1;
   }

   dev = calloc(1, sizeof(struct amdvgpu_device));
   dev->refcount = 1;
   dev->next = dev_list;
   dev_list = dev;
   dev->fd = fd;
   dev->vdev = vdev;

   simple_mtx_init(&dev->handle_to_vbo_mutex, mtx_plain);
   simple_mtx_init(&dev->contexts_mutex, mtx_plain);

   dev->handle_to_vbo = _mesa_hash_table_u64_create(NULL);

   p_atomic_set(&dev->next_blob_id, 1);

   *dev_out = dev;

   simple_mtx_unlock(&dev_mutex);

   struct drm_amdgpu_info info;
   info.return_pointer = (uintptr_t)&dev->dev_info;
   info.query = AMDGPU_INFO_DEV_INFO;
   info.return_size = sizeof(dev->dev_info);
   int r = amdvgpu_query_info(dev, &info);
   assert(r == 0);

   /* Ring idx 0 is reserved for commands running on CPU. */
   unsigned next_ring_idx = 1;
   for (unsigned i = 0; i < AMD_NUM_IP_TYPES; ++i) {
      struct drm_amdgpu_info_hw_ip ip_info = {0};
      struct drm_amdgpu_info request = {0};
      request.return_pointer = (uintptr_t)&ip_info;
      request.return_size = sizeof(ip_info);
      request.query = AMDGPU_INFO_HW_IP_INFO;
      request.query_hw_ip.type = i;
      request.query_hw_ip.ip_instance = 0;
      r = amdvgpu_query_info(dev, &request);
      if (r == 0 && ip_info.available_rings) {
         int count = util_bitcount(ip_info.available_rings);
         dev->virtio_ring_mapping[i] = next_ring_idx;
         next_ring_idx += count;
      }
   }
   /* VIRTGPU_CONTEXT_PARAM_NUM_RINGS is hardcoded for now. */
   assert(next_ring_idx <= 64);
   dev->num_virtio_rings = next_ring_idx - 1;

   dev->va_mgr = amdgpu_va_manager_alloc();
   amdgpu_va_manager_init(dev->va_mgr,
      dev->dev_info.virtual_address_offset, dev->dev_info.virtual_address_max,
      dev->dev_info.high_va_offset, dev->dev_info.high_va_max,
      dev->dev_info.virtual_address_alignment);

   _mesa_hash_table_init(&dev->contexts, NULL,
                         _mesa_hash_pointer, _mesa_key_pointer_equal);
   dev->allow_multiple_amdgpu_ctx = debug_get_bool_option("MULTIPLE_AMDGPU_CTX", false);
   dev->sync_cmd = debug_get_num_option("VIRTIO_SYNC_CMD", 0);

   *drm_major = dev->vdev->caps.version_major;
   *drm_minor = dev->vdev->caps.version_minor;

init_sync_provider:
   *p = vdrm_vpipe_get_sync(dev->vdev);
   if (!(*p))
      *p = util_sync_provider_drm(dev->fd);

   return 0;
}
