/*
 * Copyright © 2022 Imagination Technologies Ltd.
 *
 * based in part on anv driver which is:
 * Copyright © 2015 Intel Corporation
 *
 * based in part on v3dv driver which is:
 * Copyright © 2019 Raspberry Pi
 *
 * SPDX-License-Identifier: MIT
 */

#include "pvr_instance.h"

#include <fcntl.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

#include "vk_alloc.h"
#include "vk_log.h"

#include "wsi_common.h"

#include "util/build_id.h"

#include "pvr_debug.h"
#include "pvr_device.h"
#include "pvr_entrypoints.h"
#include "pvr_macros.h"
#include "pvr_physical_device.h"
#include "pvr_winsys.h"
#include "pvr_wsi.h"

#if defined(VK_USE_PLATFORM_DISPLAY_KHR)
#   define PVR_USE_WSI_PLATFORM_DISPLAY true
#else
#   define PVR_USE_WSI_PLATFORM_DISPLAY false
#endif

static const struct vk_instance_extension_table pvr_instance_extensions = {
   .KHR_device_group_creation = true,
   .KHR_display = PVR_USE_WSI_PLATFORM_DISPLAY,
   .KHR_external_fence_capabilities = true,
   .KHR_external_memory_capabilities = true,
   .KHR_external_semaphore_capabilities = true,
   .KHR_get_display_properties2 = PVR_USE_WSI_PLATFORM_DISPLAY,
   .KHR_get_physical_device_properties2 = true,
   .KHR_get_surface_capabilities2 = PVR_USE_WSI_PLATFORM,
   .KHR_surface = PVR_USE_WSI_PLATFORM,
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
   .KHR_wayland_surface = true,
#endif
#ifdef VK_USE_PLATFORM_XCB_KHR
   .KHR_xcb_surface = true,
#endif
#ifdef VK_USE_PLATFORM_XLIB_KHR
   .KHR_xlib_surface = true,
#endif
   .EXT_debug_report = true,
   .EXT_debug_utils = true,
#ifndef VK_USE_PLATFORM_WIN32_KHR
   .EXT_headless_surface = PVR_USE_WSI_PLATFORM,
#endif
};

static VkResult pvr_get_drm_devices(void *const obj,
                                    drmDevicePtr *const devices,
                                    const int max_devices,
                                    int *const num_devices_out)
{
   int ret = drmGetDevices2(0, devices, max_devices);
   if (ret < 0) {
      return vk_errorf(obj,
                       VK_ERROR_INITIALIZATION_FAILED,
                       "Failed to enumerate drm devices (errno %d: %s)",
                       -ret,
                       strerror(-ret));
   }

   if (num_devices_out)
      *num_devices_out = ret;

   return VK_SUCCESS;
}

static bool
pvr_drm_device_is_compatible(drmDevicePtr drm_dev)
{
   drmVersionPtr version;
   bool is_pvr;
   int32_t fd;

   fd = open(drm_dev->nodes[DRM_NODE_RENDER], O_RDWR | O_CLOEXEC);
   if (fd < 0) {
      mesa_logd("Failed to open render node: %s\n",
                drm_dev->nodes[DRM_NODE_RENDER]);

      return false;
   }

   version = drmGetVersion(fd);
   if (!version) {
      mesa_logd("Failed to get version information for render node: %s\n",
                drm_dev->nodes[DRM_NODE_RENDER]);

      close(fd);
      return false;
   }

   is_pvr = !strcmp(version->name, PVR_DRM_DRIVER_NAME);

#if defined(PVR_SUPPORT_SERVICES_DRIVER)
   is_pvr |= !strcmp(version->name, PVR_SRV_DRIVER_NAME);
#endif /* defined(PVR_SUPPORT_SERVICES_DRIVER) */

   drmFreeVersion(version);
   close(fd);

   return is_pvr;
}

static bool pvr_drm_device_is_compatible_display(drmDevicePtr drm_dev)
{
   uint64_t has_dumb_buffer = 0;
   uint64_t prime_caps = 0;
   bool ret = false;
   int32_t fd;

   mesa_logd("Checking DRM primary node for compatibility: %s",
             drm_dev->nodes[DRM_NODE_PRIMARY]);
   fd = open(drm_dev->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC);
   if (fd < 0) {
      mesa_logd("Failed to open display node: %s\n",
                drm_dev->nodes[DRM_NODE_PRIMARY]);
      return ret;
   }

   /* Must support KMS */
   if (!drmIsKMS(fd)) {
      mesa_logd("DRM device does not support KMS");
      goto out;
   }

   /* Must support dumb buffers, as these are used by the PVR winsys to
    * allocate device memory for PVR_WINSYS_BO_TYPE_DISPLAY buffer objects.
    */
   if (drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &has_dumb_buffer) ||
       !has_dumb_buffer) {
      mesa_logd("DRM device does not support dumb buffers");
      goto out;
   }

   /* Must support PRIME export (so GPU can import dumb buffers) */
   if (drmGetCap(fd, DRM_CAP_PRIME, &prime_caps)) {
      mesa_loge("Failed to query DRM_CAP_PRIME: %s", strerror(errno));
      goto out;
   }

   if (!(prime_caps & DRM_PRIME_CAP_EXPORT)) {
      mesa_logd("DRM device lacks PRIME export support (caps: 0x%" PRIx64 ")",
                prime_caps);
      goto out;
   }

   ret = true;

out:
   close(fd);
   return ret;
}

static VkResult
pvr_physical_device_enumerate(struct vk_instance *const vk_instance)
{
   struct pvr_instance *const instance =
      container_of(vk_instance, struct pvr_instance, vk);

   drmDevicePtr drm_display_device = NULL;
   drmDevicePtr drm_render_device = NULL;
   struct pvr_physical_device *pdevice;
   drmDevicePtr *drm_devices;
   int num_drm_devices = 0;
   VkResult result;

   result = pvr_get_drm_devices(instance, NULL, 0, &num_drm_devices);
   if (result != VK_SUCCESS)
      goto out;

   if (num_drm_devices == 0) {
      result = VK_SUCCESS;
      goto out;
   }

   drm_devices = vk_alloc(&vk_instance->alloc,
                          sizeof(*drm_devices) * num_drm_devices,
                          8,
                          VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE);
   if (!drm_devices) {
      result = vk_error(instance, VK_ERROR_OUT_OF_HOST_MEMORY);
      goto out;
   }

   result = pvr_get_drm_devices(instance, drm_devices, num_drm_devices, NULL);
   if (result != VK_SUCCESS)
      goto out_free_drm_device_ptrs;

   /* First search for our render node... */
   for (int i = 0; i < num_drm_devices; i++) {
      drmDevice *const drm_dev = drm_devices[i];

      if (drm_dev->bustype != DRM_BUS_PLATFORM)
         continue;

      if (!(drm_dev->available_nodes & BITFIELD_BIT(DRM_NODE_RENDER)))
         continue;


      if (!pvr_drm_device_is_compatible(drm_dev))
         continue;

      drm_render_device = drm_dev;
      break;
   }

   if (!drm_render_device) {
      result = VK_SUCCESS;
      goto out_free_drm_devices;
   }

   mesa_logd("Found compatible render device '%s'.",
             drm_render_device->nodes[DRM_NODE_RENDER]);

   /* ...then find a compatible display node, if available. */
   for (int i = 0; i < num_drm_devices; i++) {
      drmDevice *const drm_dev = drm_devices[i];

      if (drm_dev->bustype != DRM_BUS_PLATFORM)
         continue;

      if (!(drm_dev->available_nodes & BITFIELD_BIT(DRM_NODE_PRIMARY)))
         continue;

      if (!pvr_drm_device_is_compatible_display(drm_devices[i]))
         continue;

      drm_display_device = drm_dev;
      mesa_logd("Found a compatible display device: '%s'.",
                drm_display_device->nodes[DRM_NODE_PRIMARY]);
      break;
   }

   pdevice = vk_alloc(&vk_instance->alloc,
                      sizeof(*pdevice),
                      8,
                      VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE);
   if (!pdevice) {
      result = vk_error(instance, VK_ERROR_OUT_OF_HOST_MEMORY);
      goto out_free_drm_devices;
   }

   result = pvr_physical_device_init(pdevice,
                                     instance,
                                     drm_render_device,
                                     drm_display_device);
   if (result != VK_SUCCESS) {
      if (result == VK_ERROR_INCOMPATIBLE_DRIVER)
         result = VK_SUCCESS;

      goto err_free_pdevice;
   }

   if (PVR_IS_DEBUG_SET(INFO)) {
      pvr_physical_device_dump_info(
         pdevice,
         drm_display_device
            ? drm_display_device->deviceinfo.platform->compatible
            : NULL,
         drm_render_device->deviceinfo.platform->compatible);
   }

   list_add(&pdevice->vk.link, &vk_instance->physical_devices.list);

   result = VK_SUCCESS;
   goto out_free_drm_devices;

err_free_pdevice:
   vk_free(&vk_instance->alloc, pdevice);

out_free_drm_devices:
   drmFreeDevices(drm_devices, num_drm_devices);

out_free_drm_device_ptrs:
   vk_free(&vk_instance->alloc, drm_devices);

out:
   return result;
}

static bool
pvr_get_driver_build_sha(struct pvr_instance *instance)
{
   const struct build_id_note *note;
   unsigned build_id_len;

   note = build_id_find_nhdr_for_addr(pvr_get_driver_build_sha);
   if (!note) {
      mesa_loge("Failed to find build-id.");
      return false;
   }

   build_id_len = build_id_length(note);
   if (build_id_len < BUILD_ID_EXPECTED_HASH_LENGTH) {
      mesa_loge("Build-id too short. It needs to be a SHA.");
      return false;
   }

   STATIC_ASSERT(sizeof(instance->driver_build_sha) == BLAKE3_KEY_LEN);
   copy_build_id_to_sha1(instance->driver_build_sha, note);

   return true;
}

VkResult pvr_CreateInstance(const VkInstanceCreateInfo *pCreateInfo,
                            const VkAllocationCallbacks *pAllocator,
                            VkInstance *pInstance)
{
   struct vk_instance_dispatch_table dispatch_table;
   struct pvr_instance *instance;
   VkResult result;

   assert(pCreateInfo->sType == VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);

   if (!pAllocator)
      pAllocator = vk_default_allocator();

   instance = vk_alloc(pAllocator,
                       sizeof(*instance),
                       8,
                       VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE);
   if (!instance)
      return vk_error(NULL, VK_ERROR_OUT_OF_HOST_MEMORY);

   vk_instance_dispatch_table_from_entrypoints(&dispatch_table,
                                               &pvr_instance_entrypoints,
                                               true);

   vk_instance_dispatch_table_from_entrypoints(&dispatch_table,
                                               &wsi_instance_entrypoints,
                                               false);

   result = vk_instance_init(&instance->vk,
                             &pvr_instance_extensions,
                             &dispatch_table,
                             pCreateInfo,
                             pAllocator);
   if (result != VK_SUCCESS)
      goto err_free_instance;

   pvr_process_debug_variable();

   instance->active_device_count = 0;

   instance->vk.physical_devices.enumerate = pvr_physical_device_enumerate;
   instance->vk.physical_devices.destroy = pvr_physical_device_destroy;

   VG(VALGRIND_CREATE_MEMPOOL(instance, 0, false));

   if (!pvr_get_driver_build_sha(instance)) {
      result = vk_errorf(NULL,
                         VK_ERROR_INITIALIZATION_FAILED,
                         "Failed to get driver build sha.");
      goto err_free_instance;
   }

   *pInstance = pvr_instance_to_handle(instance);

   return VK_SUCCESS;

err_free_instance:
   vk_free(pAllocator, instance);
   return result;
}

void pvr_DestroyInstance(VkInstance _instance,
                         const VkAllocationCallbacks *pAllocator)
{
   VK_FROM_HANDLE(pvr_instance, instance, _instance);

   if (!instance)
      return;

   VG(VALGRIND_DESTROY_MEMPOOL(instance));

   vk_instance_finish(&instance->vk);
   vk_free(&instance->vk.alloc, instance);
}

VkResult pvr_EnumerateInstanceVersion(uint32_t *pApiVersion)
{
   *pApiVersion = VK_MAKE_API_VERSION(0, 1, 4, VK_HEADER_VERSION);
   return VK_SUCCESS;
}

VkResult
pvr_EnumerateInstanceExtensionProperties(const char *pLayerName,
                                         uint32_t *pPropertyCount,
                                         VkExtensionProperties *pProperties)
{
   if (pLayerName)
      return vk_error(NULL, VK_ERROR_LAYER_NOT_PRESENT);

   return vk_enumerate_instance_extension_properties(&pvr_instance_extensions,
                                                     pPropertyCount,
                                                     pProperties);
}

PFN_vkVoidFunction pvr_GetInstanceProcAddr(VkInstance _instance,
                                           const char *pName)
{
   const struct vk_instance *vk_instance = NULL;

   if (_instance != NULL) {
      VK_FROM_HANDLE(pvr_instance, instance, _instance);
      vk_instance = &instance->vk;
   }

   return vk_instance_get_proc_addr(vk_instance,
                                    &pvr_instance_entrypoints,
                                    pName);
}

/* With version 1+ of the loader interface the ICD should expose
 * vk_icdGetInstanceProcAddr to work around certain LD_PRELOAD issues seen in
 * apps.
 */
PUBLIC
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL
vk_icdGetInstanceProcAddr(VkInstance instance, const char *pName)
{
   return pvr_GetInstanceProcAddr(instance, pName);
}

VkResult pvr_EnumerateInstanceLayerProperties(uint32_t *pPropertyCount,
                                              VkLayerProperties *pProperties)
{
   if (!pProperties) {
      *pPropertyCount = 0;
      return VK_SUCCESS;
   }

   return vk_error(NULL, VK_ERROR_LAYER_NOT_PRESENT);
}
