/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2018-2026 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "../test_common.h"

std::string VKFullscreenQuadVertex = R"EOSHADER(

#version 460 core

void main()
{
	vec2 positions[] = {
		vec2(-1.0f,  1.0f),
		vec2( 1.0f,  1.0f),
		vec2(-1.0f, -1.0f),
		vec2( 1.0f, -1.0f),
	};

	gl_Position = vec4(positions[gl_VertexIndex], 0, 1);
}

)EOSHADER";

static std::string common = R"EOSHADER(

#version 460 core

#define v2f v2f_block \
{                     \
	vec4 pos;           \
	vec4 col;           \
	vec4 uv;            \
}

)EOSHADER";

std::string VKDefaultVertex = common + R"EOSHADER(

layout(location = 0) in vec3 Position;
layout(location = 1) in vec4 Color;
layout(location = 2) in vec2 UV;

layout(location = 0) out v2f vertOut;

void main()
{
	vertOut.pos = vec4(Position.xyz*vec3(1,-1,1), 1);
	gl_Position = vertOut.pos;
	vertOut.col = Color;
	vertOut.uv = vec4(UV.xy, 0, 1);
}

)EOSHADER";

std::string VKDefaultPixel = common + R"EOSHADER(

layout(location = 0) in v2f vertIn;

layout(location = 0, index = 0) out vec4 Color;

void main()
{
	Color = vertIn.col;
}

)EOSHADER";

#define VMA_IMPLEMENTATION
#define VMA_STATIC_VULKAN_FUNCTIONS 0

#define VMA_ASSERT(expr) TEST_ASSERT(expr, "VMA assertion failed");

#pragma warning(push)
#pragma warning(disable : 4127)
#pragma warning(disable : 4189)
#pragma warning(disable : 4324)
#pragma warning(disable : 4505)

#include "vk_headers.h"

#pragma warning(pop)

#include "vk_test.h"

#if defined(WIN32)
#include "../win32/win32_window.h"
#elif defined(ANDROID)
#include "../android/android_window.h"
#elif defined(__linux__)
#include "../linux/linux_window.h"
#elif defined(__APPLE__)
#include "../apple/apple_window.h"
#else
#error UNKNOWN PLATFORM
#endif

static VkBool32 VKAPI_PTR vulkanCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                                         VkDebugUtilsMessageTypeFlagsEXT messageTypes,
                                         const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData,
                                         void *pUserData)
{
  if(strstr(pCallbackData->pMessageIdName, "-01779"))
    return false;

  TEST_WARN("Vulkan message: [%s] %s", pCallbackData->pMessageIdName, pCallbackData->pMessage);

  return false;
}

VulkanGraphicsTest::VulkanGraphicsTest()
{
}

namespace
{
bool volk = false;
bool spv = false;
uint32_t vulkanVersion = 0;
VkInstance inst = VK_NULL_HANDLE;
VkPhysicalDevice selectedPhys = VK_NULL_HANDLE;
std::vector<const char *> enabledInstExts;
std::vector<const char *> enabledLayers;
};

void VulkanGraphicsTest::Prepare(int argc, char **argv)
{
  GraphicsTest::Prepare(argc, argv);

  static bool prepared = false;

  std::vector<VkLayerProperties> availInstLayers;
  std::vector<VkExtensionProperties> availInstExts;

  if(!prepared)
  {
    prepared = true;

    volk = (volkInitialize() == VK_SUCCESS);

    spv = SpvCompilationSupported();

    if(volk && spv)
    {
      enabledInstExts = instExts;
      enabledLayers = instLayers;

      enabledInstExts.push_back(VK_KHR_SURFACE_EXTENSION_NAME);

#if defined(WIN32)
      enabledInstExts.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#elif defined(ANDROID)
      enabledInstExts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#elif defined(__linux__)
      enabledInstExts.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);

      X11Window::Init();
#elif defined(__APPLE__)
#if defined(VK_USE_PLATFORM_METAL_EXT)
      enabledInstExts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
#endif
#if defined(VK_USE_PLATFORM_MACOS_MVK)
      enabledInstExts.push_back(VK_MVK_MACOS_SURFACE_EXTENSION_NAME);
#endif

      AppleWindow::Init();
#else
#error UNKNOWN PLATFORM
#endif

      std::vector<const char *> optInstExts;

      // this is used by so many sub extensions, initialise it if we can.
      optInstExts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);

      // enable debug utils when possible
      optInstExts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);

      // ditto validation features
      optInstExts.push_back(VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME);

      CHECK_VKR(vkh::enumerateInstanceLayerProperties(availInstLayers));

      if(debugDevice)
      {
        bool found = false;

        for(const VkLayerProperties &layer : availInstLayers)
        {
          if(!strcmp(layer.layerName, "VK_LAYER_KHRONOS_validation"))
          {
            enabledLayers.push_back("VK_LAYER_KHRONOS_validation");
            found = true;
            break;
          }
        }

        if(!found)
        {
          for(const VkLayerProperties &layer : availInstLayers)
          {
            if(!strcmp(layer.layerName, "VK_LAYER_LUNARG_standard_validation"))
            {
              enabledLayers.push_back("VK_LAYER_LUNARG_standard_validation");
              found = true;
              break;
            }
          }
        }
      }

      CHECK_VKR(vkh::enumerateInstanceExtensionProperties(availInstExts, NULL));

      for(const char *l : enabledLayers)
      {
        bool supported = false;
        for(const VkLayerProperties &layer : availInstLayers)
        {
          if(!strcmp(layer.layerName, l))
          {
            supported = true;
            break;
          }
        }

        if(!supported)
        {
          Avail = "Vulkan layer '";
          Avail += l;
          Avail += "' is not available";
          return;
        }

        std::vector<VkExtensionProperties> tmp;
        CHECK_VKR(vkh::enumerateInstanceExtensionProperties(tmp, l));

        for(const VkExtensionProperties &t : tmp)
          availInstExts.push_back(t);
      }

      // strip any extensions that are not supported
      for(auto it = enabledInstExts.begin(); it != enabledInstExts.end();)
      {
        bool found = false;
        for(VkExtensionProperties &ext : availInstExts)
        {
          if(!strcmp(ext.extensionName, *it))
          {
            found = true;
            break;
          }
        }

        if(found)
        {
          ++it;
        }
        else
        {
          DEBUG_BREAK();
          it = enabledInstExts.erase(it);
        }
      }

      // add any optional extensions that are supported
      for(const char *search : optInstExts)
      {
        bool found = false;
        for(VkExtensionProperties &ext : availInstExts)
        {
          if(!strcmp(ext.extensionName, search))
          {
            found = true;
            break;
          }
        }

        if(found)
          enabledInstExts.push_back(search);
      }

      VkValidationFeaturesEXT featuresEXT = {VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT};
      VkValidationFeatureEnableEXT enableFeatures[] = {
          VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
          VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT,
      };
      featuresEXT.enabledValidationFeatureCount = ARRAY_COUNT(enableFeatures);
      featuresEXT.pEnabledValidationFeatures = enableFeatures;

      // allow command line override
      for(int i = 0; i < argc; i++)
      {
        if(!strcmp(argv[i], "--gpuva"))
        {
          for(const char *a : enabledInstExts)
          {
            if(std::string(a) == VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME)
            {
              featuresEXT.pNext = instInfoNext;
              instInfoNext = &featuresEXT;
            }
          }
        }
      }

      vulkanVersion = volkGetInstanceVersion();

      vkh::ApplicationInfo app("RenderDoc autotesting", VK_MAKE_VERSION(1, 0, 0),
                               "RenderDoc autotesting", VK_MAKE_VERSION(1, 0, 0), vulkanVersion);

      TEST_LOG("Initialising Vulkan at VK%u.%u", VK_VERSION_MAJOR(vulkanVersion),
               VK_VERSION_MINOR(vulkanVersion));

      VkResult vkr = vkCreateInstance(
          vkh::InstanceCreateInfo(app, enabledLayers, enabledInstExts).next(instInfoNext), NULL,
          &inst);

      if(vkr != VK_SUCCESS)
      {
        TEST_ERROR("Error initialising vulkan instance: %d", vkr);
      }
      else
      {
        volkLoadInstance((VkInstance)inst);

        std::vector<VkPhysicalDevice> physDevices;
        CHECK_VKR(vkh::enumeratePhysicalDevices(physDevices, inst));

        std::vector<VkPhysicalDeviceProperties> physProps;
        for(VkPhysicalDevice p : physDevices)
        {
          VkPhysicalDeviceProperties props;
          vkGetPhysicalDeviceProperties(p, &props);
          physProps.push_back(props);
        }

        // default to the first discrete card
        for(size_t i = 0; i < physDevices.size(); i++)
        {
          if(physProps[i].deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
          {
            selectedPhys = physDevices[i];
            break;
          }
        }

        // if none found, default to first
        if(selectedPhys == VK_NULL_HANDLE && !physDevices.empty())
          selectedPhys = physDevices[0];

        // allow command line override
        for(int i = 0; i < argc; i++)
        {
          if(!strcmp(argv[i], "--gpu") && i + 1 < argc)
          {
            std::string needle = strlower(argv[i + 1]);

            const bool nv = (needle == "nv" || needle == "nvidia");
            const bool amd = (needle == "amd");
            const bool intel = (needle == "intel");

            for(size_t p = 0; p < physDevices.size(); p++)
            {
              std::string haystack = strlower(physProps[p].deviceName);

              if(haystack.find(needle) != std::string::npos ||
                 (nv && physProps[p].vendorID == PCI_VENDOR_NV) ||
                 (amd && physProps[p].vendorID == PCI_VENDOR_AMD) ||
                 (intel && physProps[p].vendorID == PCI_VENDOR_INTEL))
              {
                selectedPhys = physDevices[p];
                break;
              }
            }

            break;
          }
        }
      }
    }
  }

  instance = inst;
  phys = selectedPhys;

  if(!volk)
    Avail = "volk did not initialise - vulkan library is not available";
  else if(!spv)
    Avail = InternalSpvCompiler() ? "Internal SPIR-V compiler did not initialise"
                                  : "Couldn't find 'glslc' or 'glslangValidator' in PATH - "
                                    "required for SPIR-V compilation";
  else if(instance == VK_NULL_HANDLE)
    Avail = "Vulkan instance did not initialise";
  else if(phys == VK_NULL_HANDLE)
    Avail = "Couldn't find vulkan physical device";

  if(!Avail.empty())
    return;

  devExts.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);

  VkPhysicalDeviceFeatures supported;
  vkGetPhysicalDeviceFeatures(phys, &supported);

#define CHECK_FEATURE(a)                                                  \
  if(features.a && !supported.a)                                          \
  {                                                                       \
    Avail = "Required physical device feature '" #a "' is not supported"; \
    return;                                                               \
  }                                                                       \
  if(optFeatures.a && supported.a)                                        \
  {                                                                       \
    features.a = VK_TRUE;                                                 \
  }

  CHECK_FEATURE(robustBufferAccess);
  CHECK_FEATURE(fullDrawIndexUint32);
  CHECK_FEATURE(imageCubeArray);
  CHECK_FEATURE(independentBlend);
  CHECK_FEATURE(geometryShader);
  CHECK_FEATURE(tessellationShader);
  CHECK_FEATURE(sampleRateShading);
  CHECK_FEATURE(dualSrcBlend);
  CHECK_FEATURE(logicOp);
  CHECK_FEATURE(multiDrawIndirect);
  CHECK_FEATURE(drawIndirectFirstInstance);
  CHECK_FEATURE(depthClamp);
  CHECK_FEATURE(depthBiasClamp);
  CHECK_FEATURE(fillModeNonSolid);
  CHECK_FEATURE(depthBounds);
  CHECK_FEATURE(wideLines);
  CHECK_FEATURE(largePoints);
  CHECK_FEATURE(alphaToOne);
  CHECK_FEATURE(multiViewport);
  CHECK_FEATURE(samplerAnisotropy);
  CHECK_FEATURE(textureCompressionETC2);
  CHECK_FEATURE(textureCompressionASTC_LDR);
  CHECK_FEATURE(textureCompressionBC);
  CHECK_FEATURE(occlusionQueryPrecise);
  CHECK_FEATURE(pipelineStatisticsQuery);
  CHECK_FEATURE(vertexPipelineStoresAndAtomics);
  CHECK_FEATURE(fragmentStoresAndAtomics);
  CHECK_FEATURE(shaderTessellationAndGeometryPointSize);
  CHECK_FEATURE(shaderImageGatherExtended);
  CHECK_FEATURE(shaderStorageImageExtendedFormats);
  CHECK_FEATURE(shaderStorageImageMultisample);
  CHECK_FEATURE(shaderStorageImageReadWithoutFormat);
  CHECK_FEATURE(shaderStorageImageWriteWithoutFormat);
  CHECK_FEATURE(shaderUniformBufferArrayDynamicIndexing);
  CHECK_FEATURE(shaderSampledImageArrayDynamicIndexing);
  CHECK_FEATURE(shaderStorageBufferArrayDynamicIndexing);
  CHECK_FEATURE(shaderStorageImageArrayDynamicIndexing);
  CHECK_FEATURE(shaderClipDistance);
  CHECK_FEATURE(shaderCullDistance);
  CHECK_FEATURE(shaderFloat64);
  CHECK_FEATURE(shaderInt64);
  CHECK_FEATURE(shaderInt16);
  CHECK_FEATURE(shaderResourceResidency);
  CHECK_FEATURE(shaderResourceMinLod);
  CHECK_FEATURE(sparseBinding);
  CHECK_FEATURE(sparseResidencyBuffer);
  CHECK_FEATURE(sparseResidencyImage2D);
  CHECK_FEATURE(sparseResidencyImage3D);
  CHECK_FEATURE(sparseResidency2Samples);
  CHECK_FEATURE(sparseResidency4Samples);
  CHECK_FEATURE(sparseResidency8Samples);
  CHECK_FEATURE(sparseResidency16Samples);
  CHECK_FEATURE(sparseResidencyAliased);
  CHECK_FEATURE(variableMultisampleRate);
  CHECK_FEATURE(inheritedQueries);

  CHECK_VKR(vkh::enumerateInstanceLayerProperties(availInstLayers));
  CHECK_VKR(vkh::enumerateInstanceExtensionProperties(availInstExts, NULL));

  instExts = enabledInstExts;
  instLayers = enabledLayers;

  for(const char *l : instLayers)
  {
    bool layerSupported = false;
    for(const VkLayerProperties &layer : availInstLayers)
    {
      if(!strcmp(layer.layerName, l))
      {
        layerSupported = true;
        break;
      }
    }

    if(!layerSupported)
    {
      Avail = "Vulkan layer '";
      Avail += l;
      Avail += "' is not available";
      return;
    }

    std::vector<VkExtensionProperties> tmp;
    CHECK_VKR(vkh::enumerateInstanceExtensionProperties(tmp, l));

    for(const VkExtensionProperties &t : tmp)
      availInstExts.push_back(t);
  }

  for(const char *search : instExts)
  {
    bool extSupported = false;
    for(const VkExtensionProperties &e : availInstExts)
    {
      if(!strcmp(e.extensionName, search))
      {
        extSupported = true;
        break;
      }
    }

    if(!extSupported)
    {
      Avail = "instance extension '";
      Avail += search;
      Avail += "' is not available";
      return;
    }
  }

  std::vector<VkExtensionProperties> supportedExts;
  CHECK_VKR(vkh::enumerateDeviceExtensionProperties(supportedExts, phys, NULL));

  // add any optional extensions that are supported
  for(const char *search : optDevExts)
  {
    bool found = false;
    for(VkExtensionProperties &ext : supportedExts)
    {
      if(!strcmp(ext.extensionName, search))
      {
        found = true;
        break;
      }
    }

    if(found)
      devExts.push_back(search);
  }

  vkGetPhysicalDeviceProperties(phys, &physProperties);

  instVersion = vulkanVersion;
  devVersion = physProperties.apiVersion;

  if(std::find(enabledInstExts.begin(), enabledInstExts.end(),
               VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME) != enabledInstExts.end())
  {
    vkh::PhysicalDeviceProperties2KHR props2;
    vkGetPhysicalDeviceProperties2KHR(phys, props2);
    devVersion = props2.properties.apiVersion;
  }

  for(const char *search : devExts)
  {
    bool found = false;
    for(VkExtensionProperties &ext : supportedExts)
    {
      if(!strcmp(ext.extensionName, search))
      {
        found = true;
        break;
      }
    }

    if(!found)
    {
      // try the layers we're enabling
      for(const char *layer : enabledLayers)
      {
        std::vector<VkExtensionProperties> layerExts;
        CHECK_VKR(vkh::enumerateDeviceExtensionProperties(layerExts, phys, layer));

        for(VkExtensionProperties &ext : layerExts)
        {
          if(!strcmp(ext.extensionName, search))
          {
            found = true;
            break;
          }
        }

        if(found)
          break;
      }

      if(!found)
      {
        Avail = "Required device extension '";
        Avail += search;
        Avail += "' is not supported";
        return;
      }
    }
  }

  std::vector<VkQueueFamilyProperties> queueProps;
  vkh::getQueueFamilyProperties(queueProps, phys);

  for(uint32_t q = 0; q < queueProps.size(); q++)
  {
    if(queueProps[q].queueFlags & VK_QUEUE_GRAPHICS_BIT)
    {
      if(graphicsQueueFamilyIndex == ~0U)
        graphicsQueueFamilyIndex = q;
    }
    else if(queueProps[q].queueFlags & VK_QUEUE_COMPUTE_BIT)
    {
      if(computeQueueFamilyIndex == ~0U)
        computeQueueFamilyIndex = q;
    }
    else if(queueProps[q].queueFlags & VK_QUEUE_TRANSFER_BIT)
    {
      if(transferQueueFamilyIndex == ~0U)
        transferQueueFamilyIndex = q;
    }
  }

  // if no queue has been selected, find it now
  if(queueFamilyIndex == ~0U)
  {
    // try to find an exact match first
    for(uint32_t q = 0; q < queueProps.size(); q++)
    {
      VkQueueFlags flags = queueProps[q].queueFlags;

      if(flags == queueFlagsRequired)
      {
        queueFamilyIndex = q;
        queueCount = 1;
        break;
      }
    }
  }

  if(queueFamilyIndex == ~0U)
  {
    // if we didn't find an exact match, look for any that does satisfy what we want
    for(uint32_t q = 0; q < queueProps.size(); q++)
    {
      VkQueueFlags flags = queueProps[q].queueFlags;

      if(((flags & queueFlagsRequired) == queueFlagsRequired) && ((flags & queueFlagsBanned) == 0))
      {
        queueFamilyIndex = q;
        queueCount = 1;
        break;
      }
    }
  }

  if(queueFamilyIndex == ~0U)
    Avail = "No satisfactory queue family available";
}

bool VulkanGraphicsTest::Init()
{
  // parse parameters here to override parameters
  if(!GraphicsTest::Init())
    return false;

  if(debugDevice)
  {
    CHECK_VKR(vkCreateDebugUtilsMessengerEXT(
        instance,
        vkh::DebugUtilsMessengerCreateInfoEXT(&vulkanCallback, NULL,
                                              VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
                                                  VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT),
        NULL, &debugUtilsMessenger));
  }

  std::vector<VkExtensionProperties> supportedExts;
  CHECK_VKR(vkh::enumerateDeviceExtensionProperties(supportedExts, phys, NULL));

  // add any optional extensions that are supported
  for(const char *search : optDevExts)
  {
    bool found = false;
    for(VkExtensionProperties &ext : supportedExts)
    {
      if(!strcmp(ext.extensionName, search))
      {
        found = true;
        break;
      }
    }

    if(found)
      devExts.push_back(search);
  }

  const std::vector<float> priorities = {
      1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
      1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
  };

  std::vector<VkDeviceQueueCreateInfo> queueCreates = {
      vkh::DeviceQueueCreateInfo(queueFamilyIndex, queueCount, priorities),
  };

  if(queueFamilyIndex != graphicsQueueFamilyIndex && forceGraphicsQueue)
    queueCreates.push_back(vkh::DeviceQueueCreateInfo(graphicsQueueFamilyIndex, 1, priorities));
  if(queueFamilyIndex != computeQueueFamilyIndex &&
     (graphicsQueueFamilyIndex != computeQueueFamilyIndex || !forceGraphicsQueue) &&
     computeQueueFamilyIndex != ~0U && forceComputeQueue)
    queueCreates.push_back(vkh::DeviceQueueCreateInfo(computeQueueFamilyIndex, 1, priorities));
  if(queueFamilyIndex != transferQueueFamilyIndex &&
     graphicsQueueFamilyIndex != transferQueueFamilyIndex &&
     computeQueueFamilyIndex != transferQueueFamilyIndex && transferQueueFamilyIndex != ~0U &&
     forceTransferQueue)
    queueCreates.push_back(vkh::DeviceQueueCreateInfo(transferQueueFamilyIndex, 1, priorities));

  CHECK_VKR(vkCreateDevice(
      phys, vkh::DeviceCreateInfo(queueCreates, enabledLayers, devExts, features).next(devInfoNext),
      NULL, &device));

  volkLoadDevice(device);

  vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue);

  if(!headless)
  {
    mainWindow = MakeWindow(screenWidth, screenHeight, "Autotesting");

    if(!mainWindow->Initialised())
    {
      TEST_ERROR("Error creating surface");
      return false;
    }
  }

  VmaVulkanFunctions funcs = {
      vkGetInstanceProcAddr,
      vkGetDeviceProcAddr,
      vkGetPhysicalDeviceProperties,
      vkGetPhysicalDeviceMemoryProperties,
      vkAllocateMemory,
      vkFreeMemory,
      vkMapMemory,
      vkUnmapMemory,
      vkFlushMappedMemoryRanges,
      vkInvalidateMappedMemoryRanges,
      vkBindBufferMemory,
      vkBindImageMemory,
      vkGetBufferMemoryRequirements,
      vkGetImageMemoryRequirements,
      vkCreateBuffer,
      vkDestroyBuffer,
      vkCreateImage,
      vkDestroyImage,
      vkCmdCopyBuffer,
      vkGetBufferMemoryRequirements2KHR,
      vkGetImageMemoryRequirements2KHR,
      vkBindBufferMemory2KHR,
      vkBindImageMemory2KHR,
      vkGetPhysicalDeviceMemoryProperties2KHR,
      vkGetDeviceBufferMemoryRequirements,
      vkGetDeviceImageMemoryRequirements,
  };

  VmaAllocatorCreateInfo allocInfo = {};
  allocInfo.instance = instance;
  allocInfo.physicalDevice = phys;
  allocInfo.device = device;
  allocInfo.pVulkanFunctions = &funcs;
  allocInfo.vulkanApiVersion = devVersion;
  if(hasExt(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME) && vmaDedicated)
    allocInfo.flags |= VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT;
  if((hasExt(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME) || devVersion >= VK_MAKE_VERSION(1, 2, 0)) &&
     vmaBDA)
    allocInfo.flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;

  vmaCreateAllocator(&allocInfo, &allocator);

  TEST_LOG("Running Vulkan test on %s (version %d.%d)", physProperties.deviceName,
           VK_VERSION_MAJOR(physProperties.apiVersion), VK_VERSION_MINOR(physProperties.apiVersion));

  headlessCmds = new VulkanCommands(this);

  if(!headless)
  {
    VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo());

    vkh::GraphicsPipelineCreateInfo pipeCreateInfo;

    pipeCreateInfo.layout = layout;
    pipeCreateInfo.renderPass = mainWindow->rp;

    pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)};
    pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = {
        vkh::vertexAttr(0, 0, DefaultA2V, pos),
        vkh::vertexAttr(1, 0, DefaultA2V, col),
        vkh::vertexAttr(2, 0, DefaultA2V, uv),
    };

    pipeCreateInfo.stages = {
        CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"),
        CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"),
    };

    DefaultTriPipe = createGraphicsPipeline(pipeCreateInfo);

    DefaultTriVB = AllocatedBuffer(
        this,
        vkh::BufferCreateInfo(sizeof(DefaultTri),
                              VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT),
        VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));

    DefaultTriVB.upload(DefaultTri);
  }

  return true;
}

VulkanWindow *VulkanGraphicsTest::MakeWindow(int width, int height, const char *title)
{
#if defined(WIN32)
  GraphicsWindow *platWin = new Win32Window(width, height, title);
#elif defined(ANDROID)
  GraphicsWindow *platWin = new AndroidWindow(width, height, title);
#elif defined(__linux__)
  GraphicsWindow *platWin = new X11Window(width, height, 0, title);
#elif defined(__APPLE__)
  GraphicsWindow *platWin = new AppleWindow(width, height, title);
#else
#error UNKNOWN PLATFORM
#endif

  return new VulkanWindow(this, platWin);
}

void VulkanGraphicsTest::Shutdown()
{
  if(device)
  {
    vkDeviceWaitIdle(device);

    for(VkShaderModule shader : shaders)
      vkDestroyShaderModule(device, shader, NULL);

    for(VkDescriptorPool pool : descPools)
      vkDestroyDescriptorPool(device, pool, NULL);

    for(VkPipeline pipe : pipes)
      vkDestroyPipeline(device, pipe, NULL);

    for(VkFramebuffer fb : framebuffers)
      vkDestroyFramebuffer(device, fb, NULL);

    for(VkRenderPass rp : renderpasses)
      vkDestroyRenderPass(device, rp, NULL);

    for(VkImageView view : imageviews)
      vkDestroyImageView(device, view, NULL);

    for(VkBufferView view : bufferviews)
      vkDestroyBufferView(device, view, NULL);

    for(VkPipelineLayout layout : pipelayouts)
      vkDestroyPipelineLayout(device, layout, NULL);

    for(VkDescriptorSetLayout layout : setlayouts)
      vkDestroyDescriptorSetLayout(device, layout, NULL);

    for(VkSampler sampler : samplers)
      vkDestroySampler(device, sampler, NULL);

    for(auto it : imageAllocs)
      vmaDestroyImage(allocator, it.first, it.second);

    for(auto it : bufferAllocs)
      vmaDestroyBuffer(allocator, it.first, it.second);

    vmaDestroyAllocator(allocator);

    if(headlessCmds)
      delete headlessCmds;

    delete mainWindow;

    vkDestroyDevice(device, NULL);
  }

  if(debugUtilsMessenger)
    vkDestroyDebugUtilsMessengerEXT(instance, debugUtilsMessenger, NULL);

  if(instance)
    vkDestroyInstance(instance, NULL);
}

bool VulkanGraphicsTest::Running()
{
  if(!FrameLimit())
    return false;

  return mainWindow->Update();
}

VkImage VulkanGraphicsTest::StartUsingBackbuffer(VkCommandBuffer cmd, VkAccessFlags nextUse,
                                                 VkImageLayout layout, VulkanWindow *window)
{
  if(window == NULL)
    window = mainWindow;

  VkImage img = window->GetImage();

  vkh::cmdPipelineBarrier(
      cmd, {
               vkh::ImageMemoryBarrier(0, nextUse, VK_IMAGE_LAYOUT_UNDEFINED, layout, img),
           });

  return img;
}

void VulkanGraphicsTest::FinishUsingBackbuffer(VkCommandBuffer cmd, VkAccessFlags prevUse,
                                               VkImageLayout layout, VulkanWindow *window)
{
  if(window == NULL)
    window = mainWindow;

  VkImage img = window->GetImage();

  vkh::cmdPipelineBarrier(cmd, {
                                   vkh::ImageMemoryBarrier(prevUse, VK_ACCESS_MEMORY_READ_BIT, layout,
                                                           VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, img),
                               });
}

void VulkanGraphicsTest::Submit(int index, int totalSubmits, const std::vector<VkCommandBuffer> &cmds,
                                const std::vector<VkCommandBuffer> &seccmds)
{
  if(mainWindow)
    mainWindow->Submit(index, totalSubmits, cmds, seccmds, queue);
  else
    headlessCmds->Submit(cmds, seccmds, queue, VK_NULL_HANDLE, VK_NULL_HANDLE);
}

void VulkanGraphicsTest::SubmitAndPresent(const std::vector<VkCommandBuffer> &cmds)
{
  Submit(0, 1, cmds, {});
  Present();
}

void VulkanGraphicsTest::Present()
{
  mainWindow->Present(queue);
}

VkPipelineShaderStageCreateInfo VulkanGraphicsTest::LoadShaderModule(const std::string &filename,
                                                                     ShaderStage stage,
                                                                     const char *entry_point)
{
  VkShaderModule ret = VK_NULL_HANDLE;

  std::vector<uint32_t> spirv;

  FILE *f = fopen(filename.c_str(), "rb");
  if(f)
  {
    fseek(f, 0, SEEK_END);
    spirv.resize(ftell(f) / sizeof(uint32_t));
    fseek(f, 0, SEEK_SET);
    fread(spirv.data(), sizeof(uint32_t), spirv.size(), f);
    fclose(f);
  }

  if(spirv.empty())
    return {};

  CHECK_VKR(vkCreateShaderModule(device, vkh::ShaderModuleCreateInfo(spirv), NULL, &ret));

  shaders.push_back(ret);

  VkShaderStageFlagBits vkstage[] = {
      VK_SHADER_STAGE_VERTEX_BIT,
      VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT,
      VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT,
      VK_SHADER_STAGE_GEOMETRY_BIT,
      VK_SHADER_STAGE_FRAGMENT_BIT,
      VK_SHADER_STAGE_COMPUTE_BIT,
  };

  return vkh::PipelineShaderStageCreateInfo(ret, vkstage[(int)stage], entry_point);
}

VkPipelineShaderStageCreateInfo VulkanGraphicsTest::CompileShaderModule(
    const std::string &source_text, ShaderLang lang, ShaderStage stage, const char *entry_point,
    const std::map<std::string, std::string> &macros, SPIRVTarget target)
{
  VkShaderModule ret = VK_NULL_HANDLE;

  std::vector<uint32_t> spirv =
      ::CompileShaderToSpv(source_text, target, lang, stage, entry_point, macros);

  if(spirv.empty())
    return {};

  CHECK_VKR(vkCreateShaderModule(device, vkh::ShaderModuleCreateInfo(spirv), NULL, &ret));

  shaders.push_back(ret);

  VkShaderStageFlagBits vkstage[] = {
      VK_SHADER_STAGE_VERTEX_BIT,
      VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT,
      VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT,
      VK_SHADER_STAGE_GEOMETRY_BIT,
      VK_SHADER_STAGE_FRAGMENT_BIT,
      VK_SHADER_STAGE_COMPUTE_BIT,
      VK_SHADER_STAGE_MESH_BIT_EXT,
      VK_SHADER_STAGE_TASK_BIT_EXT,
  };

  return vkh::PipelineShaderStageCreateInfo(ret, vkstage[(int)stage], entry_point);
}

VkCommandBuffer VulkanGraphicsTest::GetCommandBuffer(VkCommandBufferLevel level, VulkanWindow *window)
{
  if(window == NULL)
    window = mainWindow;

  if(window)
    return window->GetCommandBuffer(level);

  return headlessCmds->GetCommandBuffer(level);
}

template <>
void VulkanGraphicsTest::setName(VkPipeline obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_PIPELINE, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkPipelineLayout obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_PIPELINE_LAYOUT, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkFramebuffer obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_FRAMEBUFFER, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkImage obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_IMAGE, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkImageView obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_IMAGE_VIEW, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkSampler obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_SAMPLER, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkBuffer obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_BUFFER, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkBufferView obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_BUFFER_VIEW, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkSemaphore obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_SEMAPHORE, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkFence obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_FENCE, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkAccelerationStructureKHR obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_KHR, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkDescriptorSetLayout obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkDescriptorSet obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkRenderPass obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_RENDER_PASS, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkCommandBuffer obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_COMMAND_BUFFER, (uint64_t)obj, name);
}

template <>
void VulkanGraphicsTest::setName(VkCommandPool obj, const std::string &name)
{
  setName(VK_OBJECT_TYPE_COMMAND_POOL, (uint64_t)obj, name);
}

void VulkanGraphicsTest::setName(VkObjectType objType, uint64_t obj, const std::string &name)
{
  if(vkSetDebugUtilsObjectNameEXT)
  {
    VkDebugUtilsObjectNameInfoEXT info = {};
    info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
    info.objectType = objType;
    info.objectHandle = obj;
    info.pObjectName = name.c_str();
    vkSetDebugUtilsObjectNameEXT(device, &info);
  }
}

void VulkanGraphicsTest::pushMarker(VkCommandBuffer cmd, const std::string &name)
{
  if(vkCmdBeginDebugUtilsLabelEXT)
  {
    VkDebugUtilsLabelEXT info = {};
    info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = name.c_str();
    vkCmdBeginDebugUtilsLabelEXT(cmd, &info);
  }
}

void VulkanGraphicsTest::setMarker(VkCommandBuffer cmd, const std::string &name)
{
  if(vkCmdInsertDebugUtilsLabelEXT)
  {
    VkDebugUtilsLabelEXT info = {};
    info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = name.c_str();
    vkCmdInsertDebugUtilsLabelEXT(cmd, &info);
  }
}

void VulkanGraphicsTest::popMarker(VkCommandBuffer cmd)
{
  if(vkCmdEndDebugUtilsLabelEXT)
    vkCmdEndDebugUtilsLabelEXT(cmd);
}

void VulkanGraphicsTest::blitToSwap(VkCommandBuffer cmd, VkImage src, VkImageLayout srcLayout,
                                    VkImage dst, VkImageLayout dstLayout)
{
  VkImageBlit region = {};
  region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  region.srcSubresource.layerCount = 1;
  region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  region.dstSubresource.layerCount = 1;
  region.srcOffsets[1].x = mainWindow->scissor.extent.width;
  region.srcOffsets[1].y = mainWindow->scissor.extent.height;
  region.srcOffsets[1].z = 1;
  region.dstOffsets[1].x = mainWindow->scissor.extent.width;
  region.dstOffsets[1].y = mainWindow->scissor.extent.height;
  region.dstOffsets[1].z = 1;

  vkCmdBlitImage(cmd, src, srcLayout, dst, dstLayout, 1, &region, VK_FILTER_LINEAR);
}

void VulkanGraphicsTest::uploadBufferToImage(VkImage destImage, VkExtent3D destExtent,
                                             VkBuffer srcBuffer, VkImageLayout finalLayout)
{
  VkCommandBuffer cmd = GetCommandBuffer();

  vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

  vkh::cmdPipelineBarrier(
      cmd, {
               vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED,
                                       VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, destImage),
           });

  VkBufferImageCopy copy = {};
  copy.imageExtent = destExtent;
  copy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  copy.imageSubresource.layerCount = 1;

  vkCmdCopyBufferToImage(cmd, srcBuffer, destImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);

  vkh::cmdPipelineBarrier(
      cmd, {
               vkh::ImageMemoryBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
                                       VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, finalLayout, destImage),
           });

  vkEndCommandBuffer(cmd);

  Submit(99, 99, {cmd});

  vkDeviceWaitIdle(device);
}

void VulkanGraphicsTest::pushMarker(VkQueue q, const std::string &name)
{
  if(vkQueueBeginDebugUtilsLabelEXT)
  {
    VkDebugUtilsLabelEXT info = {};
    info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = name.c_str();
    vkQueueBeginDebugUtilsLabelEXT(q, &info);
  }
}

void VulkanGraphicsTest::setMarker(VkQueue q, const std::string &name)
{
  if(vkQueueInsertDebugUtilsLabelEXT)
  {
    VkDebugUtilsLabelEXT info = {};
    info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = name.c_str();
    vkQueueInsertDebugUtilsLabelEXT(q, &info);
  }
}

void VulkanGraphicsTest::popMarker(VkQueue q)
{
  if(vkQueueEndDebugUtilsLabelEXT)
    vkQueueEndDebugUtilsLabelEXT(q);
}

VkDescriptorSet VulkanGraphicsTest::allocateDescriptorSet(VkDescriptorSetLayout setLayout)
{
  VkDescriptorSet ret = VK_NULL_HANDLE;

  if(!descPools.empty())
  {
    VkDescriptorPool pool = descPools.back();
    VkResult vkr =
        vkAllocateDescriptorSets(device, vkh::DescriptorSetAllocateInfo(pool, {setLayout}), &ret);
    if(vkr == VK_SUCCESS)
      return ret;
  }

  // failed to allocate, create a new pool and push it
  {
    VkDescriptorPool pool = VK_NULL_HANDLE;

    std::vector<VkDescriptorPoolSize> poolSizes = {
        {VK_DESCRIPTOR_TYPE_SAMPLER, 1024},
        {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1024},
        {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1024},
        {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1024},
        {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1024},
        {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1024},
        {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1024},
        {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1024},
        {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1024},
        {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1024},
        {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1024},
    };

    VkDescriptorPoolInlineUniformBlockCreateInfo inlineCreateInfo = {
        VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_INLINE_UNIFORM_BLOCK_CREATE_INFO,
    };
    void *next = NULL;

    if(hasExt(VK_EXT_INLINE_UNIFORM_BLOCK_EXTENSION_NAME) || devVersion >= VK_MAKE_VERSION(1, 3, 0))
    {
      poolSizes.push_back({VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK, 128 * 4096});

      inlineCreateInfo.maxInlineUniformBlockBindings = 1024;
      next = &inlineCreateInfo;
    }

    if(hasExt(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME))
    {
      poolSizes.push_back({VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, 1024});
    }

    CHECK_VKR(vkCreateDescriptorPool(
        device, vkh::DescriptorPoolCreateInfo(128, poolSizes).next(next), NULL, &pool));
    descPools.push_back(pool);

    // this must succeed or we can't continue.
    CHECK_VKR(
        vkAllocateDescriptorSets(device, vkh::DescriptorSetAllocateInfo(pool, {setLayout}), &ret));
    return ret;
  }
}

VkPipeline VulkanGraphicsTest::createGraphicsPipeline(const VkGraphicsPipelineCreateInfo *info)
{
  VkPipeline ret;
  CHECK_VKR(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, info, NULL, &ret));
  pipes.push_back(ret);
  return ret;
}

VkPipeline VulkanGraphicsTest::createComputePipeline(const VkComputePipelineCreateInfo *info)
{
  VkPipeline ret;
  CHECK_VKR(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, info, NULL, &ret));
  pipes.push_back(ret);
  return ret;
}

VkFramebuffer VulkanGraphicsTest::createFramebuffer(const VkFramebufferCreateInfo *info)
{
  VkFramebuffer ret;
  CHECK_VKR(vkCreateFramebuffer(device, info, NULL, &ret));
  framebuffers.push_back(ret);
  return ret;
}

VkRenderPass VulkanGraphicsTest::createRenderPass(const VkRenderPassCreateInfo *info)
{
  VkRenderPass ret;
  CHECK_VKR(vkCreateRenderPass(device, info, NULL, &ret));
  renderpasses.push_back(ret);
  return ret;
}

VkImageView VulkanGraphicsTest::createImageView(const VkImageViewCreateInfo *info)
{
  VkImageView ret;
  CHECK_VKR(vkCreateImageView(device, info, NULL, &ret));
  imageviews.push_back(ret);
  return ret;
}

VkBufferView VulkanGraphicsTest::createBufferView(const VkBufferViewCreateInfo *info)
{
  VkBufferView ret;
  CHECK_VKR(vkCreateBufferView(device, info, NULL, &ret));
  bufferviews.push_back(ret);
  return ret;
}

VkPipelineLayout VulkanGraphicsTest::createPipelineLayout(const VkPipelineLayoutCreateInfo *info)
{
  VkPipelineLayout ret;
  CHECK_VKR(vkCreatePipelineLayout(device, info, NULL, &ret));
  pipelayouts.push_back(ret);
  return ret;
}

VkDescriptorSetLayout VulkanGraphicsTest::createDescriptorSetLayout(
    const VkDescriptorSetLayoutCreateInfo *info)
{
  VkDescriptorSetLayout ret;
  CHECK_VKR(vkCreateDescriptorSetLayout(device, info, NULL, &ret));
  setlayouts.push_back(ret);
  return ret;
}

VkSampler VulkanGraphicsTest::createSampler(const VkSamplerCreateInfo *info)
{
  VkSampler ret;
  CHECK_VKR(vkCreateSampler(device, info, NULL, &ret));
  samplers.push_back(ret);
  return ret;
}

VulkanCommands::VulkanCommands(VulkanGraphicsTest *test)
{
  m_Test = test;

  CHECK_VKR(vkCreateCommandPool(
      m_Test->device,
      vkh::CommandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
                                 m_Test->queueFamilyIndex),
      NULL, &cmdPool));
}

VulkanCommands::~VulkanCommands()
{
  vkDestroyCommandPool(m_Test->device, cmdPool, NULL);

  for(VkFence fence : fences)
    vkDestroyFence(m_Test->device, fence, NULL);
}

VkCommandBuffer VulkanCommands::GetCommandBuffer(VkCommandBufferLevel level)
{
  std::vector<VkCommandBuffer> &buflist = freeCommandBuffers[level];

  if(buflist.empty())
  {
    buflist.resize(4);

    CHECK_VKR(vkAllocateCommandBuffers(
        m_Test->device, vkh::CommandBufferAllocateInfo(cmdPool, 4, level), &buflist[0]));
  }

  VkCommandBuffer ret = buflist.back();
  buflist.pop_back();

  return ret;
}

void VulkanCommands::Submit(const std::vector<VkCommandBuffer> &cmds,
                            const std::vector<VkCommandBuffer> &seccmds, VkQueue q,
                            VkSemaphore wait, VkSemaphore signal)
{
  VkFence fence;
  CHECK_VKR(vkCreateFence(m_Test->device, vkh::FenceCreateInfo(), NULL, &fence));

  fences.insert(fence);

  if(m_Test->hasExt(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME))
  {
    VkSubmitInfo2KHR submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO_2_KHR};

    std::vector<VkCommandBufferSubmitInfoKHR> cmdSubmits;
    for(VkCommandBuffer cmd : cmds)
      cmdSubmits.push_back({VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO_KHR, NULL, cmd, 0});

    submit.commandBufferInfoCount = (uint32_t)cmdSubmits.size();
    submit.pCommandBufferInfos = cmdSubmits.data();

    VkSemaphoreSubmitInfoKHR waitInfo = {}, signalInfo = {};

    if(wait != VK_NULL_HANDLE)
    {
      waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR;
      waitInfo.semaphore = wait;
      waitInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT_KHR;

      submit.waitSemaphoreInfoCount = 1;
      submit.pWaitSemaphoreInfos = &waitInfo;
    }

    if(signal != VK_NULL_HANDLE)
    {
      signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR;
      signalInfo.semaphore = signal;
      signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT_KHR;

      submit.signalSemaphoreInfoCount = 1;
      submit.pSignalSemaphoreInfos = &signalInfo;
    }

    CHECK_VKR(vkQueueSubmit2KHR(q, 1, &submit, fence));
  }
  else
  {
    VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;

    VkSubmitInfo submit = vkh::SubmitInfo(cmds);

    if(wait != VK_NULL_HANDLE)
    {
      submit.waitSemaphoreCount = 1;
      submit.pWaitDstStageMask = &waitStage;
      submit.pWaitSemaphores = &wait;
    }

    if(signal != VK_NULL_HANDLE)
    {
      submit.signalSemaphoreCount = 1;
      submit.pSignalSemaphores = &signal;
    }

    CHECK_VKR(vkQueueSubmit(q, 1, &submit, fence));
  }

  for(const VkCommandBuffer &cmd : cmds)
    pendingCommandBuffers[0].push_back(std::make_pair(cmd, fence));

  for(const VkCommandBuffer &cmd : seccmds)
    pendingCommandBuffers[1].push_back(std::make_pair(cmd, fence));
}

VulkanWindow::VulkanWindow(VulkanGraphicsTest *test, GraphicsWindow *win)
    : GraphicsWindow(win->title), VulkanCommands(test)
{
  m_Test = test;
  m_Win = win;

  {
    std::lock_guard<std::mutex> lock(m_Test->mutex);

#if defined(WIN32)
    VkWin32SurfaceCreateInfoKHR createInfo;

    createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
    createInfo.pNext = NULL;
    createInfo.flags = 0;
    createInfo.hwnd = ((Win32Window *)win)->wnd;
    createInfo.hinstance = GetModuleHandleA(NULL);

    vkCreateWin32SurfaceKHR(m_Test->instance, &createInfo, NULL, &surface);
#elif defined(ANDROID)
    VkAndroidSurfaceCreateInfoKHR createInfo;

    createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
    createInfo.pNext = NULL;
    createInfo.flags = 0;
    createInfo.window = ((AndroidWindow *)win)->window;

    vkCreateAndroidSurfaceKHR(m_Test->instance, &createInfo, NULL, &surface);
#elif defined(__linux__)
    VkXcbSurfaceCreateInfoKHR createInfo;

    createInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
    createInfo.pNext = NULL;
    createInfo.flags = 0;
    createInfo.connection = ((X11Window *)win)->xcb.connection;
    createInfo.window = ((X11Window *)win)->xcb.window;

    vkCreateXcbSurfaceKHR(m_Test->instance, &createInfo, NULL, &surface);
#elif defined(__APPLE__)
#if defined(VK_USE_PLATFORM_METAL_EXT)
    VkMetalSurfaceCreateInfoEXT mtlCreateInfo;
    mtlCreateInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
    mtlCreateInfo.pNext = NULL;
    mtlCreateInfo.flags = 0;
    mtlCreateInfo.pLayer = ((AppleWindow *)win)->layer;
    vkCreateMetalSurfaceEXT(m_Test->instance, &mtlCreateInfo, NULL, &surface);
#endif
#if defined(VK_USE_PLATFORM_MACOS_MVK)
    VkMacOSSurfaceCreateInfoMVK createInfo;

    createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
    createInfo.pNext = NULL;
    createInfo.flags = 0;
    createInfo.pView = ((AppleWindow *)win)->view;
    vkCreateMacOSSurfaceMVK(m_Test->instance, &createInfo, NULL, &surface);
#endif
#else
#error UNKNOWN PLATFORM
#endif
  }

  CreateSwapchain();

  Acquire();
}

VulkanWindow::~VulkanWindow()
{
  DestroySwapchain();

  {
    TEST_ASSERT(renderStartSemaphore.size() == renderEndSemaphore.size(),
                "size mismatch between start/end semaphore");
    TEST_ASSERT(renderStartSemaphore.size() == imageFences.size(),
                "size mismatch between render/image semaphore");

    for(size_t i = 0; i < renderStartSemaphore.size(); i++)
    {
      vkDestroySemaphore(m_Test->device, renderStartSemaphore[i], NULL);
      vkDestroySemaphore(m_Test->device, renderEndSemaphore[i], NULL);
      vkDestroyFence(m_Test->device, imageFences[i], NULL);
    }

    if(surface)
      vkDestroySurfaceKHR(m_Test->instance, surface, NULL);
  }

  delete m_Win;
}

void VulkanWindow::setViewScissor(VkCommandBuffer cmd)
{
  vkCmdSetViewport(cmd, 0, 1, &viewport);
  vkCmdSetScissor(cmd, 0, 1, &scissor);
}

bool VulkanWindow::CreateSwapchain()
{
  std::lock_guard<std::mutex> lock(m_Test->mutex);

  if(surface == VK_NULL_HANDLE)
    return false;

  VkResult vkr = VK_SUCCESS;

  VkSurfaceFormatKHR surfaceFormat = {};

  std::vector<VkSurfaceFormatKHR> formats;
  CHECK_VKR(vkh::getSurfaceFormatsKHR(formats, m_Test->phys, surface));

  VkBool32 support = VK_FALSE;
  CHECK_VKR(vkGetPhysicalDeviceSurfaceSupportKHR(m_Test->phys, m_Test->queueFamilyIndex, surface,
                                                 &support));
  TEST_ASSERT(support, "Presentation is not supported on surface");

  if(vkr != VK_SUCCESS || formats.empty())
  {
    TEST_ERROR("Error getting surface formats: %s", vkh::result_str(vkr));
    return false;
  }

  surfaceFormat = formats[0];

  for(const VkSurfaceFormatKHR &f : formats)
  {
    if((f.format == VK_FORMAT_B8G8R8A8_SRGB || f.format == VK_FORMAT_R8G8B8A8_SRGB) &&
       f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
    {
      surfaceFormat = f;
      break;
    }
  }

  if(surfaceFormat.format == VK_FORMAT_UNDEFINED)
  {
    surfaceFormat.format = VK_FORMAT_B8G8R8A8_SRGB;
    surfaceFormat.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
  }

  format = surfaceFormat.format;

  std::vector<VkPresentModeKHR> modes;
  CHECK_VKR(vkh::getSurfacePresentModesKHR(modes, m_Test->phys, surface));

  VkPresentModeKHR mode = VK_PRESENT_MODE_IMMEDIATE_KHR;

  if(std::find(modes.begin(), modes.end(), mode) == modes.end())
    mode = VK_PRESENT_MODE_FIFO_KHR;

  uint32_t width = 1, height = 1;

  VkSurfaceCapabilitiesKHR capabilities;
  CHECK_VKR(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_Test->phys, surface, &capabilities));

  width = capabilities.currentExtent.width;

  width = std::min(width, capabilities.maxImageExtent.width);
  width = std::max(width, capabilities.minImageExtent.width);

  height = capabilities.currentExtent.height;

  height = std::min(height, capabilities.maxImageExtent.height);
  height = std::max(height, capabilities.minImageExtent.height);

  viewport = vkh::Viewport(0, 0, (float)width, (float)height, 0.0f, 1.0f);
  scissor = vkh::Rect2D({0, 0}, {width, height});

  CHECK_VKR(vkCreateSwapchainKHR(
      m_Test->device,
      vkh::SwapchainCreateInfoKHR(surface, mode, surfaceFormat, {width, height},
                                  VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
                                  VK_NULL_HANDLE, capabilities.minImageCount),
      NULL, &swap));

  CHECK_VKR(vkh::getSwapchainImagesKHR(imgs, m_Test->device, swap));

  if(rp == VK_NULL_HANDLE)
  {
    vkh::RenderPassCreator renderPassCreateInfo;

    renderPassCreateInfo.attachments.push_back(
        vkh::AttachmentDescription(format, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL));

    renderPassCreateInfo.addSubpass({VkAttachmentReference({0, VK_IMAGE_LAYOUT_GENERAL})});

    rp = m_Test->createRenderPass(renderPassCreateInfo);
  }

  renderStartSemaphore.resize(imgs.size());
  renderEndSemaphore.resize(imgs.size());
  imageFences.resize(imgs.size());
  for(size_t i = 0; i < renderStartSemaphore.size(); i++)
  {
    CHECK_VKR(vkCreateSemaphore(m_Test->device, vkh::SemaphoreCreateInfo(), NULL,
                                &renderStartSemaphore[i]));
    CHECK_VKR(vkCreateSemaphore(m_Test->device, vkh::SemaphoreCreateInfo(), NULL,
                                &renderEndSemaphore[i]));

    m_Test->setName(renderStartSemaphore[i], title + " renderStartSemaphore" + std::to_string(i));
    m_Test->setName(renderEndSemaphore[i], title + " renderEndSemaphore" + std::to_string(i));

    CHECK_VKR(vkCreateFence(m_Test->device, vkh::FenceCreateInfo(VK_FENCE_CREATE_SIGNALED_BIT),
                            NULL, &imageFences[i]));

    m_Test->setName(imageFences[i], title + " fence" + std::to_string(i));
  }

  imgviews.resize(imgs.size());
  for(size_t i = 0; i < imgs.size(); i++)
  {
    CHECK_VKR(vkCreateImageView(m_Test->device,
                                vkh::ImageViewCreateInfo(imgs[i], VK_IMAGE_VIEW_TYPE_2D, format),
                                NULL, &imgviews[i]));
  }
  fbs.resize(imgs.size());
  for(size_t i = 0; i < imgviews.size(); i++)
    fbs[i] = m_Test->createFramebuffer(vkh::FramebufferCreateInfo(rp, {imgviews[i]}, scissor.extent));

  return true;
}

void VulkanWindow::Acquire()
{
  if(swap == VK_NULL_HANDLE)
    return;

  semIdx = (semIdx + 1) % renderStartSemaphore.size();

  // acquire next image stupidly does not properly block, do a manual block
  vkWaitForFences(m_Test->device, 1, &imageFences[semIdx], VK_FALSE, UINT64_MAX);
  vkResetFences(m_Test->device, 1, &imageFences[semIdx]);

  VkResult vkr = vkAcquireNextImageKHR(m_Test->device, swap, UINT64_MAX,
                                       renderStartSemaphore[semIdx], imageFences[semIdx], &imgIndex);

  if(vkr == VK_SUBOPTIMAL_KHR || vkr == VK_ERROR_OUT_OF_DATE_KHR)
  {
    DestroySwapchain();
    CreateSwapchain();

    vkr = vkAcquireNextImageKHR(m_Test->device, swap, UINT64_MAX, renderStartSemaphore[semIdx],
                                VK_NULL_HANDLE, &imgIndex);
  }
}

void VulkanWindow::Submit(int index, int totalSubmits, const std::vector<VkCommandBuffer> &cmds,
                          const std::vector<VkCommandBuffer> &seccmds, VkQueue q)
{
  VkSemaphore signal = VK_NULL_HANDLE, wait = VK_NULL_HANDLE;

  if(index == 0)
    wait = renderStartSemaphore[semIdx];
  if(index == totalSubmits - 1)
    signal = renderEndSemaphore[semIdx];

  VulkanCommands::Submit(cmds, seccmds, q, wait, signal);
}

void VulkanWindow::MultiPresent(VkQueue queue, std::vector<VulkanWindow *> windows)
{
  std::vector<VkSwapchainKHR> swaps;
  std::vector<uint32_t> idxs;
  std::vector<VkSemaphore> waitSems;
  std::vector<VkResult> vkrs;

  for(auto it : windows)
  {
    if(it->swap == VK_NULL_HANDLE)
      continue;

    swaps.push_back(it->swap);
    idxs.push_back(it->imgIndex);
    waitSems.push_back(it->renderEndSemaphore[it->semIdx]);
    vkrs.push_back(VK_SUCCESS);
  }

  if(swaps.empty())
    return;

  VkPresentInfoKHR info = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
  info.swapchainCount = (uint32_t)swaps.size();
  info.waitSemaphoreCount = (uint32_t)waitSems.size();
  info.pSwapchains = swaps.data();
  info.pImageIndices = idxs.data();
  info.pWaitSemaphores = waitSems.data();
  info.pResults = vkrs.data();

  vkQueuePresentKHR(queue, &info);

  size_t i = 0;
  for(auto it : windows)
  {
    if(it->swap == VK_NULL_HANDLE)
      continue;

    it->PostPresent(vkrs[i++]);
  }
}

void VulkanWindow::Present(VkQueue queue)
{
  if(swap == VK_NULL_HANDLE)
    return;

  VkResult vkr =
      vkQueuePresentKHR(queue, vkh::PresentInfoKHR(swap, imgIndex, &renderEndSemaphore[semIdx]));

  PostPresent(vkr);
}

void VulkanWindow::PostPresent(VkResult vkr)
{
  if(vkr == VK_SUBOPTIMAL_KHR || vkr == VK_ERROR_OUT_OF_DATE_KHR)
  {
    DestroySwapchain();
    CreateSwapchain();
  }
  else if(vkr != VK_SUCCESS)
  {
    VkResult queuePresentError = vkr;
    CHECK_VKR(queuePresentError);
  }

  VulkanCommands::ProcessCompletions();

  Acquire();
}

void VulkanCommands::ProcessCompletions()
{
  std::set<VkFence> doneFences;
  std::map<VkFence, VkResult> fenceStatus;

  // only test each fence once so we avoid the problem of testing a fence once, finding it's not
  // ready, then testing it again in a second use and finding that it's now ready, and deleting
  // it
  for(VkFence f : fences)
    fenceStatus[f] = vkGetFenceStatus(m_Test->device, f);

  for(int level = 0; level < 2; level++)
  {
    for(auto it = pendingCommandBuffers[level].begin(); it != pendingCommandBuffers[level].end();)
    {
      if(fenceStatus[it->second] == VK_SUCCESS)
      {
        freeCommandBuffers[level].push_back(it->first);
        doneFences.insert(it->second);
        it = pendingCommandBuffers[level].erase(it);
      }
      else
      {
        ++it;
      }
    }
  }

  for(auto it = doneFences.begin(); it != doneFences.end(); ++it)
  {
    vkDestroyFence(m_Test->device, *it, NULL);
    fences.erase(*it);
  }
}

void VulkanWindow::DestroySwapchain()
{
  std::lock_guard<std::mutex> lock(m_Test->mutex);

  vkDeviceWaitIdle(m_Test->device);

  for(size_t i = 0; i < imgs.size(); i++)
    vkDestroyImageView(m_Test->device, imgviews[i], NULL);

  vkDestroySwapchainKHR(m_Test->device, swap, NULL);
}

void VulkanGraphicsTest::getPhysFeatures2(void *nextStruct)
{
  for(const char *ext : enabledInstExts)
  {
    if(!strcmp(ext, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME))
    {
      vkGetPhysicalDeviceFeatures2KHR(phys, vkh::PhysicalDeviceFeatures2KHR().next(nextStruct));
      return;
    }
  }
}

void VulkanGraphicsTest::getPhysProperties2(void *nextStruct)
{
  for(const char *ext : enabledInstExts)
  {
    if(!strcmp(ext, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME))
    {
      vkGetPhysicalDeviceProperties2KHR(phys, vkh::PhysicalDeviceProperties2KHR().next(nextStruct));
      return;
    }
  }
}

bool VulkanGraphicsTest::hasExt(const char *ext)
{
  return std::find_if(devExts.begin(), devExts.end(),
                      [ext](const char *a) { return !strcmp(a, ext); }) != devExts.end();
}

AllocatedImage::AllocatedImage(VulkanGraphicsTest *test, const VkImageCreateInfo &imgInfo,
                               const VmaAllocationCreateInfo &allocInfo)
{
  createInfo = imgInfo;
  this->test = test;
  allocator = test->allocator;
  vmaCreateImage(allocator, &imgInfo, &allocInfo, &image, &alloc, NULL);

  test->imageAllocs[image] = alloc;
}

void AllocatedImage::free()
{
  vmaDestroyImage(allocator, image, alloc);
  test->imageAllocs.erase(image);
}

AllocatedBuffer::AllocatedBuffer(VulkanGraphicsTest *test, const VkBufferCreateInfo &bufInfo,
                                 const VmaAllocationCreateInfo &allocInfo, uint32_t alignment)
{
  this->test = test;
  allocator = test->allocator;
  vmaCreateBufferWithAlignment(allocator, &bufInfo, &allocInfo, alignment, &buffer, &alloc, NULL);

  test->bufferAllocs[buffer] = alloc;

  if(bufInfo.usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT)
  {
    VkBufferDeviceAddressInfoKHR info = {
        VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO_KHR,
        NULL,
        buffer,
    };
    address = vkGetBufferDeviceAddressKHR(test->device, &info);
  }
}

void AllocatedBuffer::free()
{
  vmaDestroyBuffer(allocator, buffer, alloc);
  test->bufferAllocs.erase(buffer);
}
