#include "config.h"

#include "gskvulkandeviceprivate.h"

#include "gskgpuglobalsopprivate.h"
#include "gskgpushaderopprivate.h"
#include "gskvulkanbufferprivate.h"
#include "gskvulkanimageprivate.h"
#include "gskvulkanycbcrprivate.h"

#include "gdk/gdkdisplayprivate.h"
#include "gdk/gdkvulkancontextprivate.h"
#include "gdk/gdkprofilerprivate.h"

#define GDK_ARRAY_NAME descriptor_pools
#define GDK_ARRAY_TYPE_NAME DescriptorPools
#define GDK_ARRAY_ELEMENT_TYPE VkDescriptorPool
#define GDK_ARRAY_PREALLOC 4
#define GDK_ARRAY_NO_MEMSET 1
#include "gdk/gdkarrayimpl.c"

struct _GskVulkanDevice
{
  GskGpuDevice parent_instance;

  GskVulkanAllocator *allocators[VK_MAX_MEMORY_TYPES];
  GskVulkanAllocator *external_allocator;
  GdkVulkanFeatures features;

  GHashTable *render_pass_cache;

  VkCommandPool vk_command_pool;
  DescriptorPools descriptor_pools;
  gsize last_pool;
  VkSampler vk_samplers[GSK_GPU_SAMPLER_N_SAMPLERS];
  VkDescriptorSetLayout vk_image_set_layout;
  VkDescriptorSetLayout vk_mask_set_layout;
  VkPipelineLayout default_vk_pipeline_layout;
};

struct _GskVulkanDeviceClass
{
  GskGpuDeviceClass parent_class;
};

G_DEFINE_TYPE (GskVulkanDevice, gsk_vulkan_device, GSK_TYPE_GPU_DEVICE)

typedef struct _ConversionCacheEntry ConversionCacheEntry;
typedef struct _PipelineCacheKey PipelineCacheKey;
typedef struct _RenderPassCacheKey RenderPassCacheKey;

struct _ConversionCacheEntry
{
  VkFormat vk_format;

  /* actual data */
  VkSamplerYcbcrConversion vk_conversion;
  VkSampler vk_sampler;
};

struct _PipelineCacheKey
{
  const GskGpuShaderOpClass *op_class;
  GskGpuShaderFlags flags;
  GskGpuColorStates color_states;
  guint32 variation;
  GskGpuBlend blend;
  VkFormat vk_format;
  VkPipelineLayout vk_layout;
  VkPipeline vk_pipeline;
};

struct _RenderPassCacheKey
{
  VkFormat format;
  VkAttachmentLoadOp vk_load_op;
  VkImageLayout from_layout;
  VkImageLayout to_layout;
  VkRenderPass render_pass;
};

static guint
render_pass_cache_key_hash (gconstpointer data)
{
  const RenderPassCacheKey *key = data;

  return (key->from_layout << 26) ^
         (key->to_layout << 18) ^
         (key->vk_load_op << 16) ^
         (key->format);
}

static gboolean
render_pass_cache_key_equal (gconstpointer a,
                             gconstpointer b)
{
  const RenderPassCacheKey *keya = a;
  const RenderPassCacheKey *keyb = b;

  return keya->from_layout == keyb->from_layout &&
         keya->to_layout == keyb->to_layout &&
         keya->vk_load_op == keyb->vk_load_op &&
         keya->format == keyb->format;
}

static VkDescriptorSetLayout
gsk_vulkan_device_create_vk_image_set_layout (GskVulkanDevice *self,
                                              gsize            n_descriptors)
{
  VkDevice vk_device;
  VkDescriptorSetLayout result;

  vk_device = gsk_vulkan_device_get_vk_device (self);

  GSK_VK_CHECK (vkCreateDescriptorSetLayout, vk_device,
                                             &(VkDescriptorSetLayoutCreateInfo) {
                                                 .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
                                                 .bindingCount = 1,
                                                 .flags = 0,
                                                 .pBindings = (VkDescriptorSetLayoutBinding[1]) {
                                                     {
                                                         .binding = 0,
                                                         .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                                                         .descriptorCount = n_descriptors,
                                                         .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
                                                     }
                                                 },
                                             },
                                             NULL,
                                             &result);

  return result;
}

VkPipelineLayout
gsk_vulkan_device_create_vk_pipeline_layout (GskVulkanDevice       *self,
                                             VkDescriptorSetLayout  image1_layout,
                                             VkDescriptorSetLayout  image2_layout)
{
  VkDevice vk_device;
  VkPipelineLayout result;

  vk_device = gsk_vulkan_device_get_vk_device (self);

  GSK_VK_CHECK (vkCreatePipelineLayout, vk_device,
                                        &(VkPipelineLayoutCreateInfo) {
                                            .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
                                            .setLayoutCount = 3,
                                            .pSetLayouts = (VkDescriptorSetLayout[3]) {
                                                image1_layout,
                                                image2_layout,
                                                self->vk_mask_set_layout,
                                            },
                                            .pushConstantRangeCount = 1,
                                            .pPushConstantRanges = (VkPushConstantRange[1]) {
                                                {
                                                    .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
                                                    .offset = 0,
                                                    .size = sizeof (GskGpuGlobalsInstance)
                                                }
                                            }
                                        },
                                        NULL,
                                        &result);

  return result;
}

static GskGpuImage *
gsk_vulkan_device_create_offscreen_image (GskGpuDevice   *device,
                                          gboolean        with_mipmap,
                                          GdkMemoryFormat format,
                                          gboolean        is_srgb,
                                          gsize           width,
                                          gsize           height)
{
  GskVulkanDevice *self = GSK_VULKAN_DEVICE (device);

  return gsk_vulkan_image_new_for_offscreen (self,
                                             with_mipmap,
                                             format,
                                             is_srgb,
                                             width,
                                             height);
}

static GskGpuImage *
gsk_vulkan_device_create_atlas_image (GskGpuDevice *device,
                                      gsize         width,
                                      gsize         height)
{
  GskVulkanDevice *self = GSK_VULKAN_DEVICE (device);

  return gsk_vulkan_image_new_for_atlas (self,
                                         width,
                                         height);
}

static GskGpuImage *
gsk_vulkan_device_create_upload_image (GskGpuDevice     *device,
                                       gboolean          with_mipmap,
                                       GdkMemoryFormat   format,
                                       GskGpuConversion  conv,
                                       gsize             width,
                                       gsize             height)
{
  GskVulkanDevice *self = GSK_VULKAN_DEVICE (device);

  return gsk_vulkan_image_new_for_upload (self,
                                          with_mipmap,
                                          format,
                                          conv,
                                          width,
                                          height);
}

static GskGpuImage *
gsk_vulkan_device_create_download_image (GskGpuDevice   *device,
                                         GdkMemoryDepth  depth,
                                         gsize           width,
                                         gsize           height)
{
  GskVulkanDevice *self = GSK_VULKAN_DEVICE (device);
  GskGpuImage *image;

#ifdef HAVE_DMABUF
  image = gsk_vulkan_image_new_dmabuf (self,
                                       gdk_memory_depth_get_format (depth),
                                       FALSE,
                                       width,
                                       height);
  if (image != NULL)
    return image;
#endif

  image = gsk_vulkan_image_new_for_offscreen (self,
                                              FALSE,
                                              gdk_memory_depth_get_format (depth),
                                              FALSE,
                                              width,
                                              height);

  return image;
}

static void
gsk_vulkan_device_make_current (GskGpuDevice *device)
{
}

static void
gsk_vulkan_device_finalize (GObject *object)
{
  GskVulkanDevice *self = GSK_VULKAN_DEVICE (object);
  GskGpuDevice *device = GSK_GPU_DEVICE (self);
  VkDevice vk_device;
  GdkDisplay *display;
  GHashTableIter iter;
  gpointer key, value;
  gsize i;

  display = gsk_gpu_device_get_display (device);
  vk_device = gsk_vulkan_device_get_vk_device (self);

  g_object_steal_data (G_OBJECT (display), "-gsk-vulkan-device");

  g_hash_table_iter_init (&iter, self->render_pass_cache);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      vkDestroyRenderPass (vk_device,
                           ((RenderPassCacheKey *)key)->render_pass,
                           NULL);
      g_free (key);
    }
  g_hash_table_unref (self->render_pass_cache);

  for (i = 0; i < G_N_ELEMENTS (self->vk_samplers); i++)
    {
      if (self->vk_samplers[i] != VK_NULL_HANDLE)
        vkDestroySampler (vk_device,
                          self->vk_samplers[i],
                          NULL);
    }

  vkDestroyPipelineLayout (vk_device,
                           self->default_vk_pipeline_layout,
                           NULL);
  vkDestroyDescriptorSetLayout (vk_device,
                                self->vk_image_set_layout,
                                NULL);
  vkDestroyDescriptorSetLayout (vk_device,
                                self->vk_mask_set_layout,
                                NULL);
  for (i = 0; i < descriptor_pools_get_size (&self->descriptor_pools); i++)
    vkDestroyDescriptorPool (vk_device,
                             descriptor_pools_get (&self->descriptor_pools, i),
                             NULL);
  descriptor_pools_clear (&self->descriptor_pools);
  vkDestroyCommandPool (vk_device,
                        self->vk_command_pool,
                        NULL);

  for (i = 0; i < VK_MAX_MEMORY_TYPES; i++)
    g_clear_pointer (&self->allocators[i], gsk_vulkan_allocator_unref);
  g_clear_pointer (&self->external_allocator, gsk_vulkan_allocator_unref);

  G_OBJECT_CLASS (gsk_vulkan_device_parent_class)->finalize (object);
}

static void
gsk_vulkan_device_class_init (GskVulkanDeviceClass *klass)
{
  GskGpuDeviceClass *gpu_device_class = GSK_GPU_DEVICE_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  gpu_device_class->create_offscreen_image = gsk_vulkan_device_create_offscreen_image;
  gpu_device_class->create_atlas_image = gsk_vulkan_device_create_atlas_image;
  gpu_device_class->create_upload_image = gsk_vulkan_device_create_upload_image;
  gpu_device_class->create_download_image = gsk_vulkan_device_create_download_image;
  gpu_device_class->make_current = gsk_vulkan_device_make_current;

  object_class->finalize = gsk_vulkan_device_finalize;
}

static void
gsk_vulkan_device_init (GskVulkanDevice *self)
{
  self->render_pass_cache = g_hash_table_new (render_pass_cache_key_hash, render_pass_cache_key_equal);

  descriptor_pools_init (&self->descriptor_pools);
}

static void
gsk_vulkan_device_create_vk_objects (GskVulkanDevice *self)
{
  VkDevice vk_device;

  vk_device = gsk_vulkan_device_get_vk_device (self);

  GSK_VK_CHECK (vkCreateCommandPool, vk_device,
                                     &(const VkCommandPoolCreateInfo) {
                                         .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
                                         .queueFamilyIndex = gsk_vulkan_device_get_vk_queue_family_index (self),
                                         .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
                                     },
                                     NULL,
                                     &self->vk_command_pool);

  self->vk_image_set_layout = gsk_vulkan_device_create_vk_image_set_layout (self, 3);
  self->vk_mask_set_layout = gsk_vulkan_device_create_vk_image_set_layout (self, 1);

  self->default_vk_pipeline_layout = gsk_vulkan_device_create_vk_pipeline_layout (self,
                                                                                  self->vk_image_set_layout,
                                                                                  self->vk_image_set_layout);
}

static void
gsk_vulkan_device_setup (GskVulkanDevice *self,
                         GdkDisplay      *display)
{
  VkPhysicalDeviceProperties2 vk_props = {
    .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
    .pNext = NULL
  };

  vkGetPhysicalDeviceProperties2 (display->vk_physical_device, &vk_props);
  gsk_gpu_device_setup (GSK_GPU_DEVICE (self),
                        display,
                        vk_props.properties.limits.maxImageDimension2D,
                        GSK_GPU_DEVICE_DEFAULT_TILE_SIZE,
                        vk_props.properties.limits.minUniformBufferOffsetAlignment);
}

GskGpuDevice *
gsk_vulkan_device_get_for_display (GdkDisplay  *display,
                                   GError     **error)
{
  GskVulkanDevice *self;

  self = g_object_get_data (G_OBJECT (display), "-gsk-vulkan-device");
  if (self)
    return GSK_GPU_DEVICE (g_object_ref (self));

  if (!gdk_display_prepare_vulkan (display, error))
    return NULL;

  self = g_object_new (GSK_TYPE_VULKAN_DEVICE, NULL);
  self->features = display->vulkan_features;

  gsk_vulkan_device_setup (self, display);

  gsk_vulkan_device_create_vk_objects (self);

  g_object_set_data (G_OBJECT (display), "-gsk-vulkan-device", self);

  return GSK_GPU_DEVICE (self);
}

gboolean
gsk_vulkan_device_has_feature (GskVulkanDevice   *self,
                               GdkVulkanFeatures  feature)
{
  return (self->features & feature) == feature;
}

VkDevice
gsk_vulkan_device_get_vk_device (GskVulkanDevice *self)
{
  return gsk_gpu_device_get_display (GSK_GPU_DEVICE (self))->vk_device;
}

VkPhysicalDevice
gsk_vulkan_device_get_vk_physical_device (GskVulkanDevice *self)
{
  return gsk_gpu_device_get_display (GSK_GPU_DEVICE (self))->vk_physical_device;
}

VkQueue
gsk_vulkan_device_get_vk_queue (GskVulkanDevice *self)
{
  return gsk_gpu_device_get_display (GSK_GPU_DEVICE (self))->vk_queue;
}

uint32_t
gsk_vulkan_device_get_vk_queue_family_index (GskVulkanDevice *self)
{
  return gsk_gpu_device_get_display (GSK_GPU_DEVICE (self))->vk_queue_family_index;
}

VkDescriptorSetLayout
gsk_vulkan_device_get_vk_image_set_layout (GskVulkanDevice *self)
{
  return self->vk_image_set_layout;
}

VkDescriptorSetLayout
gsk_vulkan_device_get_vk_mask_set_layout (GskVulkanDevice *self)
{
  return self->vk_mask_set_layout;
}

VkPipelineLayout
gsk_vulkan_device_get_default_vk_pipeline_layout (GskVulkanDevice *self)
{
  return self->default_vk_pipeline_layout;
}

VkPipelineLayout
gsk_vulkan_device_get_vk_pipeline_layout (GskVulkanDevice *self,
                                          GskVulkanYcbcr  *ycbcr0,
                                          GskVulkanYcbcr  *ycbcr1)
{
  if (ycbcr0 == VK_NULL_HANDLE)
    {
      if (ycbcr1 == VK_NULL_HANDLE)
        return self->default_vk_pipeline_layout;
      else
        return gsk_vulkan_ycbcr_get_vk_pipeline_layout (ycbcr1, 1);
    }
  else
    {
      if (ycbcr1 == VK_NULL_HANDLE)
        return gsk_vulkan_ycbcr_get_vk_pipeline_layout (ycbcr0, 0);
      else
        {
          /* FIXME: someone write a test plz */
          g_assert_not_reached ();
        }
    }
}

VkCommandPool
gsk_vulkan_device_get_vk_command_pool (GskVulkanDevice *self)
{
  return self->vk_command_pool;
}

VkDescriptorSet
gsk_vulkan_device_allocate_descriptor (GskVulkanDevice             *self,
                                       const VkDescriptorSetLayout  layout,
                                       gsize                       *out_pool_id)
{
  VkDescriptorSet result;
  VkDevice vk_device;
  VkDescriptorPool new_pool;
  gsize i, n;

  vk_device = gsk_vulkan_device_get_vk_device (self);
  n = descriptor_pools_get_size (&self->descriptor_pools);

  for (i = 0; i < n; i++)
    {
      gsize pool_id = (i + self->last_pool) % n;
      VkResult res = vkAllocateDescriptorSets (vk_device,
                                               &(VkDescriptorSetAllocateInfo) {
                                                   .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
                                                   .descriptorPool = descriptor_pools_get (&self->descriptor_pools, pool_id),
                                                   .descriptorSetCount = 1,
                                                   .pSetLayouts = (VkDescriptorSetLayout[1]) {
                                                       layout,
                                                   },
                                              },
                                              &result);
      if (res == VK_SUCCESS)
        {
          self->last_pool = pool_id;
          *out_pool_id = self->last_pool;
          return result;
        }
      else if (res == VK_ERROR_OUT_OF_POOL_MEMORY ||
               res == VK_ERROR_FRAGMENTED_POOL)
        {
          continue;
        }
      else
        {
          gsk_vulkan_handle_result (res, "vkAllocateDescriptorSets");
        }
    }

  GSK_VK_CHECK (vkCreateDescriptorPool, vk_device,
                                        &(VkDescriptorPoolCreateInfo) {
                                            .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
                                            .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
                                            .maxSets = 100,
                                            .poolSizeCount = 1,
                                            .pPoolSizes = (VkDescriptorPoolSize[1]) {
                                                {
                                                    .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                                                    .descriptorCount = 100,
                                                },
                                            }
                                        },
                                        NULL,
                                        &new_pool);
  descriptor_pools_append (&self->descriptor_pools, new_pool);
  GSK_VK_CHECK (vkAllocateDescriptorSets, vk_device,
                                          &(VkDescriptorSetAllocateInfo) {
                                              .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
                                              .descriptorPool = new_pool,
                                              .descriptorSetCount = 1,
                                              .pSetLayouts = (VkDescriptorSetLayout[1]) {
                                                  layout,
                                              },
                                          },
                                          &result);

  self->last_pool = descriptor_pools_get_size (&self->descriptor_pools) - 1;
  *out_pool_id = self->last_pool;

  return result;
}

void
gsk_vulkan_device_free_descriptor (GskVulkanDevice *self,
                                   gsize            pool_id,
                                   VkDescriptorSet  set)
{
  vkFreeDescriptorSets (gsk_vulkan_device_get_vk_device (self),
                        descriptor_pools_get (&self->descriptor_pools, pool_id),
                        1,
                        &set);
}

static VkSampler
gsk_vulkan_device_create_sampler (GskVulkanDevice          *self,
                                  VkSamplerYcbcrConversion  vk_conversion,
                                  VkFilter                  vk_filter,
                                  VkSamplerAddressMode      vk_address_mode,
                                  VkSamplerMipmapMode       vk_mipmap_mode,
                                  float                     max_lod)
{
  VkSampler result;

  GSK_VK_CHECK (vkCreateSampler, gsk_vulkan_device_get_vk_device (self),
                                 &(VkSamplerCreateInfo) {
                                     .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
                                     .magFilter = vk_filter,
                                     .minFilter = vk_filter,
                                     .mipmapMode = vk_mipmap_mode,
                                     .addressModeU = vk_address_mode,
                                     .addressModeV = vk_address_mode,
                                     .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
                                     .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
                                     .unnormalizedCoordinates = VK_FALSE,
                                     .maxAnisotropy = 1.0,
                                     .minLod = 0.0,
                                     .maxLod = max_lod,
                                     .pNext = vk_conversion == VK_NULL_HANDLE ? NULL : &(VkSamplerYcbcrConversionInfo) {
                                         .sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO,
                                         .conversion = vk_conversion
                                     }
                                 },
                                 NULL,
                                 &result);

  return result;
}

VkSampler
gsk_vulkan_device_get_vk_sampler (GskVulkanDevice     *self,
                                  GskGpuSampler        sampler)
{
  const struct {
    VkFilter filter;
    VkSamplerAddressMode address_mode;
    VkSamplerMipmapMode mipmap_mode;
    float max_lod;
  } filter_attrs[GSK_GPU_SAMPLER_N_SAMPLERS] = {
      [GSK_GPU_SAMPLER_DEFAULT] = {
          .filter = VK_FILTER_LINEAR,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
          .max_lod = 0.0f,
      },
      [GSK_GPU_SAMPLER_TRANSPARENT] = {
          .filter = VK_FILTER_LINEAR,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
          .max_lod = 0.0f,
      },
      [GSK_GPU_SAMPLER_REPEAT] = {
          .filter = VK_FILTER_LINEAR,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_REPEAT,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
          .max_lod = 0.0f,
      },
      [GSK_GPU_SAMPLER_REFLECT] = {
          .filter = VK_FILTER_LINEAR,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
          .max_lod = 0.0f,
      },
      [GSK_GPU_SAMPLER_NEAREST] = {
          .filter = VK_FILTER_NEAREST,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
          .max_lod = 0.0f,
      },
      [GSK_GPU_SAMPLER_MIPMAP_DEFAULT] = {
          .filter = VK_FILTER_LINEAR,
          .address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
          .mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
          .max_lod = VK_LOD_CLAMP_NONE,
      },
  };

  if (self->vk_samplers[sampler] == VK_NULL_HANDLE)
    {
      self->vk_samplers[sampler] = gsk_vulkan_device_create_sampler (self,
                                                                     VK_NULL_HANDLE,
                                                                     filter_attrs[sampler].filter,
                                                                     filter_attrs[sampler].address_mode,
                                                                     filter_attrs[sampler].mipmap_mode,
                                                                     filter_attrs[sampler].max_lod);
    }

  return self->vk_samplers[sampler];
}

VkRenderPass
gsk_vulkan_device_get_vk_render_pass (GskVulkanDevice    *self,
                                      VkFormat            format,
                                      VkAttachmentLoadOp  vk_load_op,
                                      VkImageLayout       from_layout,
                                      VkImageLayout       to_layout)
{
  RenderPassCacheKey cache_key;
  RenderPassCacheKey *cached_result;
  VkRenderPass render_pass;
  GdkDisplay *display;

  cache_key = (RenderPassCacheKey) {
    .format = format,
    .vk_load_op = vk_load_op,
    .from_layout = from_layout,
    .to_layout = to_layout,
  };
  cached_result = g_hash_table_lookup (self->render_pass_cache, &cache_key);
  if (cached_result)
    return cached_result->render_pass;

  display = gsk_gpu_device_get_display (GSK_GPU_DEVICE (self));

  GSK_VK_CHECK (vkCreateRenderPass, display->vk_device,
                                    &(VkRenderPassCreateInfo) {
                                        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
                                        .attachmentCount = 1,
                                        .pAttachments = (VkAttachmentDescription[]) {
                                           {
                                              .format = format,
                                              .samples = VK_SAMPLE_COUNT_1_BIT,
                                              .loadOp = vk_load_op,
                                              .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
                                              .initialLayout = from_layout,
                                              .finalLayout = to_layout
                                           }
                                        },
                                        .subpassCount = 1,
                                        .pSubpasses = (VkSubpassDescription []) {
                                           {
                                              .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
                                              .inputAttachmentCount = 0,
                                              .colorAttachmentCount = 1,
                                              .pColorAttachments = (VkAttachmentReference []) {
                                                 {
                                                    .attachment = 0,
                                                     .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
                                                  }
                                               },
                                               .pResolveAttachments = (VkAttachmentReference []) {
                                                  {
                                                     .attachment = VK_ATTACHMENT_UNUSED,
                                                     .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
                                                  }
                                               },
                                               .pDepthStencilAttachment = NULL,
                                            }
                                         },
                                         .dependencyCount = 0,
                                      },
                                      NULL,
                                      &render_pass);

  cached_result = g_memdup2 (&cache_key, sizeof (RenderPassCacheKey));
  cached_result->render_pass = render_pass;

  g_hash_table_insert (self->render_pass_cache, cached_result, cached_result);

  return render_pass;
}

GskVulkanAllocator *
gsk_vulkan_device_get_allocator (GskVulkanDevice    *self,
                                 gsize               index)
{
  if (self->allocators[index] == NULL)
    {
      VkPhysicalDeviceMemoryProperties properties;

      vkGetPhysicalDeviceMemoryProperties (gsk_vulkan_device_get_vk_physical_device (self),
                                           &properties);

      self->allocators[index] = gsk_vulkan_direct_allocator_new (gsk_vulkan_device_get_vk_device (self),
                                                                 index,
                                                                 &properties.memoryTypes[index]);
      self->allocators[index] = gsk_vulkan_buddy_allocator_new (self->allocators[index],
                                                                1024 * 1024);
      //allocators[index] = gsk_vulkan_stats_allocator_new (allocators[index]);
    }

  return self->allocators[index];
}

/* following code found in
 * https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceMemoryProperties.html */
 gsize
 gsk_vulkan_device_find_allocator (GskVulkanDevice        *self,
                                   uint32_t                allowed_types,
                                   VkMemoryPropertyFlags   required_flags,
                                   VkMemoryPropertyFlags   desired_flags)
{
  VkPhysicalDeviceMemoryProperties properties;
  uint32_t i, found;

  vkGetPhysicalDeviceMemoryProperties (gsk_vulkan_device_get_vk_physical_device (self),
                                       &properties);

  found = properties.memoryTypeCount;
  for (i = 0; i < properties.memoryTypeCount; i++)
    {
      if (!(allowed_types & (1 << i)))
        continue;

      if ((properties.memoryTypes[i].propertyFlags & required_flags) != required_flags)
        continue;

      found = MIN (i, found);

      if ((properties.memoryTypes[i].propertyFlags & desired_flags) == desired_flags)
        {
          found = i;
          break;
        }
  }

  g_assert (found < properties.memoryTypeCount);

  return found;
}

GskVulkanAllocator *
gsk_vulkan_device_get_external_allocator (GskVulkanDevice *self)
{
  if (self->external_allocator == NULL)
    {
      self->external_allocator = gsk_vulkan_external_allocator_new (gsk_vulkan_device_get_vk_device (self));
      //self->external_allocator = gsk_vulkan_stats_allocator_new (self->external_allocator);
    }

  return self->external_allocator;
}
