/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2016-2017 - Hans-Kristian Arntzen
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdint.h>
#include <math.h>
#include <string.h>

#include <compat/strl.h>
#include <gfx/scaler/scaler.h>
#include <gfx/video_frame.h>
#include <formats/image.h>
#include <retro_inline.h>
#include <retro_miscellaneous.h>
#include <retro_math.h>
#include <retro_assert.h>
#include <string/stdstring.h>
#include <libretro.h>

#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif

#ifdef HAVE_MENU
#include "../../menu/menu_driver.h"
#endif
#ifdef HAVE_GFX_WIDGETS
#include "../gfx_widgets.h"
#endif

#include "../font_driver.h"

#include "../common/vulkan_common.h"

#include "../../driver.h"
#include "../../configuration.h"
#ifdef HAVE_REWIND
#include "../../state_manager.h"
#endif

#include "../../retroarch.h"
#include "../../verbosity.h"
#include "../../record/record_driver.h"

#include "../video_coord_array.h"

static struct vk_descriptor_manager vulkan_create_descriptor_manager(
      VkDevice device,
      const VkDescriptorPoolSize *sizes,
      unsigned num_sizes,
      VkDescriptorSetLayout set_layout)
{
   int i;
   struct vk_descriptor_manager manager;

   retro_assert(num_sizes <= VULKAN_MAX_DESCRIPTOR_POOL_SIZES);

   manager.current    = NULL;
   manager.count      = 0;

   for (i = 0; i < VULKAN_MAX_DESCRIPTOR_POOL_SIZES; i++)
   {
      manager.sizes[i].type            = VK_DESCRIPTOR_TYPE_SAMPLER;
      manager.sizes[i].descriptorCount = 0;
   }
   memcpy(manager.sizes, sizes, num_sizes * sizeof(*sizes));
   manager.set_layout = set_layout;
   manager.num_sizes  = num_sizes;

   manager.head       = vulkan_alloc_descriptor_pool(device, &manager);
   retro_assert(manager.head);
   return manager;
}

static void vulkan_destroy_descriptor_manager(
      VkDevice device,
      struct vk_descriptor_manager *manager)
{
   struct vk_descriptor_pool *node = manager->head;

   while (node)
   {
      struct vk_descriptor_pool *next = node->next;

      vkFreeDescriptorSets(device, node->pool,
            VULKAN_DESCRIPTOR_MANAGER_BLOCK_SETS, node->sets);
      vkDestroyDescriptorPool(device, node->pool, NULL);

      free(node);
      node = next;
   }

   memset(manager, 0, sizeof(*manager));
}

static struct vk_buffer_chain vulkan_buffer_chain_init(
      VkDeviceSize block_size,
      VkDeviceSize alignment,
      VkBufferUsageFlags usage)
{
   struct vk_buffer_chain chain;

   chain.block_size = block_size;
   chain.alignment  = alignment;
   chain.offset     = 0;
   chain.usage      = usage;
   chain.head       = NULL;
   chain.current    = NULL;

   return chain;
}



static void vulkan_set_viewport(void *data, unsigned viewport_width,
      unsigned viewport_height, bool force_full, bool allow_rotate);
static bool vulkan_is_mapped_swapchain_texture_ptr(const vk_t* vk,
      const void* ptr);

#ifdef HAVE_OVERLAY
static void vulkan_overlay_free(vk_t *vk);
static void vulkan_render_overlay(vk_t *vk, unsigned width, unsigned height);
#endif
static void vulkan_viewport_info(void *data, struct video_viewport *vp);

static const gfx_ctx_driver_t *vulkan_get_context(vk_t *vk, settings_t *settings)
{
   void                 *ctx_data  = NULL;
   unsigned major                  = 1;
   unsigned minor                  = 0;
   enum gfx_ctx_api api            = GFX_CTX_VULKAN_API;
   const gfx_ctx_driver_t *gfx_ctx = video_context_driver_init_first(
         vk, settings->arrays.video_context_driver,
         api, major, minor, false, &ctx_data);

   if (ctx_data)
      vk->ctx_data = ctx_data;

   return gfx_ctx;
}

static void vulkan_init_render_pass(
      vk_t *vk)
{
   VkRenderPassCreateInfo rp_info;
   VkAttachmentReference color_ref;
   VkAttachmentDescription attachment;
   VkSubpassDescription subpass;

   attachment.flags             = 0;
   /* Backbuffer format. */
   attachment.format            = vk->context->swapchain_format;
   /* Not multisampled. */
   attachment.samples           = VK_SAMPLE_COUNT_1_BIT;
   /* When starting the frame, we want tiles to be cleared. */
   attachment.loadOp            = VK_ATTACHMENT_LOAD_OP_CLEAR;
   /* When end the frame, we want tiles to be written out. */
   attachment.storeOp           = VK_ATTACHMENT_STORE_OP_STORE;
   /* Don't care about stencil since we're not using it. */
   attachment.stencilLoadOp     = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
   attachment.stencilStoreOp    = VK_ATTACHMENT_STORE_OP_DONT_CARE;

   /* The image layout will be attachment_optimal
    * when we're executing the renderpass. */
   attachment.initialLayout     = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
   attachment.finalLayout       = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

   /* Color attachment reference */
   color_ref.attachment         = 0;
   color_ref.layout             = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

   /* We have one subpass.
    * This subpass has 1 color attachment. */
   subpass.flags                    = 0;
   subpass.pipelineBindPoint        = VK_PIPELINE_BIND_POINT_GRAPHICS;
   subpass.inputAttachmentCount     = 0;
   subpass.pInputAttachments        = NULL;
   subpass.colorAttachmentCount     = 1;
   subpass.pColorAttachments        = &color_ref;
   subpass.pResolveAttachments      = NULL;
   subpass.pDepthStencilAttachment  = NULL;
   subpass.preserveAttachmentCount  = 0;
   subpass.pPreserveAttachments     = NULL;

   /* Finally, create the renderpass. */
   rp_info.sType                = 
      VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
   rp_info.pNext                = NULL;
   rp_info.flags                = 0;
   rp_info.attachmentCount      = 1;
   rp_info.pAttachments         = &attachment;
   rp_info.subpassCount         = 1;
   rp_info.pSubpasses           = &subpass;
   rp_info.dependencyCount      = 0;
   rp_info.pDependencies        = NULL;

   vkCreateRenderPass(vk->context->device,
         &rp_info, NULL, &vk->render_pass);
}

static void vulkan_init_framebuffers(
      vk_t *vk)
{
   int i;

   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      VkImageViewCreateInfo view =
      { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
      VkFramebufferCreateInfo info =
      { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };

      vk->backbuffers[i].image = vk->context->swapchain_images[i];

      if (vk->context->swapchain_images[i] == VK_NULL_HANDLE)
      {
         vk->backbuffers[i].view        = VK_NULL_HANDLE;
         vk->backbuffers[i].framebuffer = VK_NULL_HANDLE;
         continue;
      }

      /* Create an image view which we can render into. */
      view.viewType                        = VK_IMAGE_VIEW_TYPE_2D;
      view.format                          = vk->context->swapchain_format;
      view.image                           = vk->backbuffers[i].image;
      view.subresourceRange.baseMipLevel   = 0;
      view.subresourceRange.baseArrayLayer = 0;
      view.subresourceRange.levelCount     = 1;
      view.subresourceRange.layerCount     = 1;
      view.subresourceRange.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
      view.components.r                    = VK_COMPONENT_SWIZZLE_R;
      view.components.g                    = VK_COMPONENT_SWIZZLE_G;
      view.components.b                    = VK_COMPONENT_SWIZZLE_B;
      view.components.a                    = VK_COMPONENT_SWIZZLE_A;

      vkCreateImageView(vk->context->device,
            &view, NULL, &vk->backbuffers[i].view);

      /* Create the framebuffer */
      info.renderPass      = vk->render_pass;
      info.attachmentCount = 1;
      info.pAttachments    = &vk->backbuffers[i].view;
      info.width           = vk->context->swapchain_width;
      info.height          = vk->context->swapchain_height;
      info.layers          = 1;

      vkCreateFramebuffer(vk->context->device,
            &info, NULL, &vk->backbuffers[i].framebuffer);
   }
}

static void vulkan_init_pipeline_layout(
      vk_t *vk)
{
   VkPipelineLayoutCreateInfo layout_info;
   VkDescriptorSetLayoutCreateInfo set_layout_info;
   VkDescriptorSetLayoutBinding bindings[3];

   bindings[0].binding            = 0;
   bindings[0].descriptorType     = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
   bindings[0].descriptorCount    = 1;
   bindings[0].stageFlags         = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
   bindings[0].pImmutableSamplers = NULL;

   bindings[1].binding            = 1;
   bindings[1].descriptorType     = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
   bindings[1].descriptorCount    = 1;
   bindings[1].stageFlags         = VK_SHADER_STAGE_FRAGMENT_BIT;
   bindings[1].pImmutableSamplers = NULL;
   
   bindings[2].binding            = 2;
   bindings[2].descriptorType     = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
   bindings[2].descriptorCount    = 1;
   bindings[2].stageFlags         = VK_SHADER_STAGE_FRAGMENT_BIT;
   bindings[2].pImmutableSamplers = NULL;

   set_layout_info.sType          = 
      VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
   set_layout_info.pNext          = NULL;
   set_layout_info.flags          = 0;
   set_layout_info.bindingCount   = 3;
   set_layout_info.pBindings      = bindings;

   vkCreateDescriptorSetLayout(vk->context->device,
         &set_layout_info, NULL, &vk->pipelines.set_layout);

   layout_info.sType                  = 
      VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
   layout_info.pNext                  = NULL;
   layout_info.flags                  = 0;
   layout_info.setLayoutCount         = 1;
   layout_info.pSetLayouts            = &vk->pipelines.set_layout;
   layout_info.pushConstantRangeCount = 0;
   layout_info.pPushConstantRanges    = NULL;

   vkCreatePipelineLayout(vk->context->device,
         &layout_info, NULL, &vk->pipelines.layout);
}

static void vulkan_init_pipelines(vk_t *vk)
{
#ifdef VULKAN_HDR_SWAPCHAIN
   static const uint32_t hdr_frag[] =
#include "vulkan_shaders/hdr.frag.inc"
      ;
#endif /* VULKAN_HDR_SWAPCHAIN */

   static const uint32_t alpha_blend_vert[] =
#include "vulkan_shaders/alpha_blend.vert.inc"
      ;

   static const uint32_t alpha_blend_frag[] =
#include "vulkan_shaders/alpha_blend.frag.inc"
      ;

   static const uint32_t font_frag[] =
#include "vulkan_shaders/font.frag.inc"
      ;

   static const uint32_t pipeline_ribbon_vert[] =
#include "vulkan_shaders/pipeline_ribbon.vert.inc"
      ;

   static const uint32_t pipeline_ribbon_frag[] =
#include "vulkan_shaders/pipeline_ribbon.frag.inc"
      ;

   static const uint32_t pipeline_ribbon_simple_vert[] =
#include "vulkan_shaders/pipeline_ribbon_simple.vert.inc"
      ;

   static const uint32_t pipeline_ribbon_simple_frag[] =
#include "vulkan_shaders/pipeline_ribbon_simple.frag.inc"
      ;

   static const uint32_t pipeline_snow_simple_frag[] =
#include "vulkan_shaders/pipeline_snow_simple.frag.inc"
      ;

   static const uint32_t pipeline_snow_frag[] =
#include "vulkan_shaders/pipeline_snow.frag.inc"
      ;

   static const uint32_t pipeline_bokeh_frag[] =
#include "vulkan_shaders/pipeline_bokeh.frag.inc"
      ;

   int i;
   VkPipelineInputAssemblyStateCreateInfo input_assembly = {
      VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO };
   VkPipelineVertexInputStateCreateInfo vertex_input     = {
      VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
   VkPipelineRasterizationStateCreateInfo raster         = {
      VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO };
   VkPipelineColorBlendAttachmentState blend_attachment  = {0};
   VkPipelineColorBlendStateCreateInfo blend             = {
      VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO };
   VkPipelineViewportStateCreateInfo viewport            = {
      VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO };
   VkPipelineDepthStencilStateCreateInfo depth_stencil   = {
      VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
   VkPipelineMultisampleStateCreateInfo multisample      = {
      VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO };
   VkPipelineDynamicStateCreateInfo dynamic              = {
      VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO };

   VkPipelineShaderStageCreateInfo shader_stages[2]      = {
      { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO },
      { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO },
   };

   VkGraphicsPipelineCreateInfo pipe                     = {
      VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
   VkShaderModuleCreateInfo module_info                  = {
      VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
   VkVertexInputAttributeDescription attributes[3]       = {{0}};
   VkVertexInputBindingDescription binding               = {0};

   static const VkDynamicState dynamics[] = {
      VK_DYNAMIC_STATE_VIEWPORT,
      VK_DYNAMIC_STATE_SCISSOR,
   };

   vulkan_init_pipeline_layout(vk);

   /* Input assembly */
   input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

   /* VAO state */
   attributes[0].location  = 0;
   attributes[0].binding   = 0;
   attributes[0].format    = VK_FORMAT_R32G32_SFLOAT;
   attributes[0].offset    = 0;
   attributes[1].location  = 1;
   attributes[1].binding   = 0;
   attributes[1].format    = VK_FORMAT_R32G32_SFLOAT;
   attributes[1].offset    = 2 * sizeof(float);
   attributes[2].location  = 2;
   attributes[2].binding   = 0;
   attributes[2].format    = VK_FORMAT_R32G32B32A32_SFLOAT;
   attributes[2].offset    = 4 * sizeof(float);

   binding.binding         = 0;
   binding.stride          = sizeof(struct vk_vertex);
   binding.inputRate       = VK_VERTEX_INPUT_RATE_VERTEX;

   vertex_input.vertexBindingDescriptionCount   = 1;
   vertex_input.pVertexBindingDescriptions      = &binding;
   vertex_input.vertexAttributeDescriptionCount = 3;
   vertex_input.pVertexAttributeDescriptions    = attributes;

   /* Raster state */
   raster.polygonMode                   = VK_POLYGON_MODE_FILL;
   raster.cullMode                      = VK_CULL_MODE_NONE;
   raster.frontFace                     = VK_FRONT_FACE_COUNTER_CLOCKWISE;
   raster.depthClampEnable              = false;
   raster.rasterizerDiscardEnable       = false;
   raster.depthBiasEnable               = false;
   raster.lineWidth                     = 1.0f;

   /* Blend state */
   blend_attachment.blendEnable         = false;
   blend_attachment.colorWriteMask      = 0xf;
   blend.attachmentCount                = 1;
   blend.pAttachments                   = &blend_attachment;

   /* Viewport state */
   viewport.viewportCount               = 1;
   viewport.scissorCount                = 1;

   /* Depth-stencil state */
   depth_stencil.depthTestEnable        = false;
   depth_stencil.depthWriteEnable       = false;
   depth_stencil.depthBoundsTestEnable  = false;
   depth_stencil.stencilTestEnable      = false;
   depth_stencil.minDepthBounds         = 0.0f;
   depth_stencil.maxDepthBounds         = 1.0f;

   /* Multisample state */
   multisample.rasterizationSamples     = VK_SAMPLE_COUNT_1_BIT;

   /* Dynamic state */
   dynamic.pDynamicStates               = dynamics;
   dynamic.dynamicStateCount            = ARRAY_SIZE(dynamics);

   pipe.stageCount                      = 2;
   pipe.pStages                         = shader_stages;
   pipe.pVertexInputState               = &vertex_input;
   pipe.pInputAssemblyState             = &input_assembly;
   pipe.pRasterizationState             = &raster;
   pipe.pColorBlendState                = &blend;
   pipe.pMultisampleState               = &multisample;
   pipe.pViewportState                  = &viewport;
   pipe.pDepthStencilState              = &depth_stencil;
   pipe.pDynamicState                   = &dynamic;
   pipe.renderPass                      = vk->render_pass;
   pipe.layout                          = vk->pipelines.layout;

   module_info.codeSize                 = sizeof(alpha_blend_vert);
   module_info.pCode                    = alpha_blend_vert;
   shader_stages[0].stage               = VK_SHADER_STAGE_VERTEX_BIT;
   shader_stages[0].pName               = "main";
   vkCreateShaderModule(vk->context->device,
         &module_info, NULL, &shader_stages[0].module);

   blend_attachment.blendEnable         = true;
   blend_attachment.colorWriteMask      = 0xf;
   blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
   blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
   blend_attachment.colorBlendOp        = VK_BLEND_OP_ADD;
   blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
   blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
   blend_attachment.alphaBlendOp        = VK_BLEND_OP_ADD;

   /* Glyph pipeline */
   module_info.codeSize                 = sizeof(font_frag);
   module_info.pCode                    = font_frag;
   shader_stages[1].stage               = VK_SHADER_STAGE_FRAGMENT_BIT;
   shader_stages[1].pName               = "main";
   vkCreateShaderModule(vk->context->device,
         &module_info, NULL, &shader_stages[1].module);

   vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
         1, &pipe, NULL, &vk->pipelines.font);
   vkDestroyShaderModule(vk->context->device, shader_stages[1].module, NULL);

   /* Alpha-blended pipeline. */
   module_info.codeSize   = sizeof(alpha_blend_frag);
   module_info.pCode      = alpha_blend_frag;
   shader_stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
   shader_stages[1].pName = "main";
   vkCreateShaderModule(vk->context->device,
         &module_info, NULL, &shader_stages[1].module);

   vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
         1, &pipe, NULL, &vk->pipelines.alpha_blend);

   /* Build display pipelines. */
   for (i = 0; i < 4; i++)
   {
      input_assembly.topology = i & 2 ?
         VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP :
         VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
      blend_attachment.blendEnable = i & 1;
      vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
            1, &pipe, NULL, &vk->display.pipelines[i]);
   }

   vkDestroyShaderModule(vk->context->device, shader_stages[1].module, NULL);

#ifdef VULKAN_HDR_SWAPCHAIN
   blend_attachment.blendEnable = false;

   /* HDR pipeline. */
   module_info.codeSize   = sizeof(hdr_frag);
   module_info.pCode      = hdr_frag;
   shader_stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
   shader_stages[1].pName = "main";
   vkCreateShaderModule(vk->context->device,
         &module_info, NULL, &shader_stages[1].module);

   vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
         1, &pipe, NULL, &vk->pipelines.hdr);

   /* Build display hdr pipelines. */
   for (i = 4; i < 6; i++)
   {
      input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
      vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
            1, &pipe, NULL, &vk->display.pipelines[i]);
   }
   
   vkDestroyShaderModule(vk->context->device, shader_stages[1].module, NULL);

   blend_attachment.blendEnable = true; 
#endif /* VULKAN_HDR_SWAPCHAIN */

   vkDestroyShaderModule(vk->context->device, shader_stages[0].module, NULL);

   /* Other menu pipelines. */
   for (i = 0; i < ARRAY_SIZE(vk->display.pipelines) - 6; i++)
   {
      switch (i >> 1)
      {
         case 0:
            module_info.codeSize   = sizeof(pipeline_ribbon_vert);
            module_info.pCode      = pipeline_ribbon_vert;
            break;

         case 1:
            module_info.codeSize   = sizeof(pipeline_ribbon_simple_vert);
            module_info.pCode      = pipeline_ribbon_simple_vert;
            break;

         case 2:
            module_info.codeSize   = sizeof(alpha_blend_vert);
            module_info.pCode      = alpha_blend_vert;
            break;

         case 3:
            module_info.codeSize   = sizeof(alpha_blend_vert);
            module_info.pCode      = alpha_blend_vert;
            break;

         case 4:
            module_info.codeSize   = sizeof(alpha_blend_vert);
            module_info.pCode      = alpha_blend_vert;
            break;

         default:
            retro_assert(0 && "No shader for menu pipeline.");
            break;
      }

      shader_stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
      shader_stages[0].pName = "main";
      vkCreateShaderModule(vk->context->device,
            &module_info, NULL, &shader_stages[0].module);

      switch (i >> 1)
      {
         case 0:
            module_info.codeSize   = sizeof(pipeline_ribbon_frag);
            module_info.pCode      = pipeline_ribbon_frag;
            break;

         case 1:
            module_info.codeSize   = sizeof(pipeline_ribbon_simple_frag);
            module_info.pCode      = pipeline_ribbon_simple_frag;
            break;

         case 2:
            module_info.codeSize   = sizeof(pipeline_snow_simple_frag);
            module_info.pCode      = pipeline_snow_simple_frag;
            break;

         case 3:
            module_info.codeSize   = sizeof(pipeline_snow_frag);
            module_info.pCode      = pipeline_snow_frag;
            break;

         case 4:
            module_info.codeSize   = sizeof(pipeline_bokeh_frag);
            module_info.pCode      = pipeline_bokeh_frag;
            break;

         default:
            retro_assert(0 && "No shader for menu pipeline.");
            break;
      }

      shader_stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
      shader_stages[1].pName = "main";
      vkCreateShaderModule(vk->context->device,
            &module_info, NULL, &shader_stages[1].module);

      switch (i >> 1)
      {
         case 0:
         case 1:
            blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
            blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
            break;
         default:
            blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
            blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
            break;
      }

      input_assembly.topology = i & 1 ?
         VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP :
         VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

      vkCreateGraphicsPipelines(vk->context->device, vk->pipelines.cache,
            1, &pipe, NULL, &vk->display.pipelines[6 + i]);

      vkDestroyShaderModule(vk->context->device, shader_stages[0].module, NULL);
      vkDestroyShaderModule(vk->context->device, shader_stages[1].module, NULL);
   }
}

static void vulkan_init_samplers(vk_t *vk)
{
   VkSamplerCreateInfo info;

   info.sType                   = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
   info.pNext                   = NULL;
   info.flags                   = 0;
   info.magFilter               = VK_FILTER_NEAREST;
   info.minFilter               = VK_FILTER_NEAREST;
   info.mipmapMode              = VK_SAMPLER_MIPMAP_MODE_NEAREST;
   info.addressModeU            = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
   info.addressModeV            = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
   info.addressModeW            = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
   info.mipLodBias              = 0.0f;
   info.anisotropyEnable        = false;
   info.maxAnisotropy           = 1.0f;
   info.compareEnable           = false;
   info.minLod                  = 0.0f;
   info.maxLod                  = 0.0f;
   info.borderColor             = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
   info.unnormalizedCoordinates = false;
   vkCreateSampler(vk->context->device,
         &info, NULL, &vk->samplers.nearest);

   info.magFilter               = VK_FILTER_LINEAR;
   info.minFilter               = VK_FILTER_LINEAR;
   vkCreateSampler(vk->context->device,
         &info, NULL, &vk->samplers.linear);

   info.maxLod                  = VK_LOD_CLAMP_NONE;
   info.magFilter               = VK_FILTER_NEAREST;
   info.minFilter               = VK_FILTER_NEAREST;
   info.mipmapMode              = VK_SAMPLER_MIPMAP_MODE_NEAREST;
   vkCreateSampler(vk->context->device,
         &info, NULL, &vk->samplers.mipmap_nearest);

   info.magFilter               = VK_FILTER_LINEAR;
   info.minFilter               = VK_FILTER_LINEAR;
   info.mipmapMode              = VK_SAMPLER_MIPMAP_MODE_LINEAR;
   vkCreateSampler(vk->context->device,
         &info, NULL, &vk->samplers.mipmap_linear);
}

static void vulkan_buffer_chain_free(
      VkDevice device,
      struct vk_buffer_chain *chain)
{
   struct vk_buffer_node *node = chain->head;
   while (node)
   {
      struct vk_buffer_node *next = node->next;
      vulkan_destroy_buffer(device, &node->buffer);

      free(node);
      node = next;
   }
   memset(chain, 0, sizeof(*chain));
}


static void vulkan_deinit_buffers(vk_t *vk)
{
   int i;
   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      vulkan_buffer_chain_free(
            vk->context->device, &vk->swapchain[i].vbo);
      vulkan_buffer_chain_free(
            vk->context->device, &vk->swapchain[i].ubo);
   }
}

static void vulkan_deinit_descriptor_pool(vk_t *vk)
{
   int i;
   for (i = 0; i < vk->num_swapchain_images; i++)
      vulkan_destroy_descriptor_manager(
            vk->context->device,
            &vk->swapchain[i].descriptor_manager);
}

static void vulkan_init_textures(vk_t *vk)
{
   const uint32_t zero = 0;

   if (!(vk->flags & VK_FLAG_HW_ENABLE))
   {
      int i;
      for (i = 0; i < vk->num_swapchain_images; i++)
      {
         vk->swapchain[i].texture = vulkan_create_texture(
               vk, NULL, vk->tex_w, vk->tex_h, vk->tex_fmt,
               NULL, NULL, VULKAN_TEXTURE_STREAMED);

         {
            struct vk_texture *texture = &vk->swapchain[i].texture;
            VK_MAP_PERSISTENT_TEXTURE(vk->context->device, texture);
         }

         if (vk->swapchain[i].texture.type == VULKAN_TEXTURE_STAGING)
            vk->swapchain[i].texture_optimal = vulkan_create_texture(
                  vk, NULL, vk->tex_w, vk->tex_h, vk->tex_fmt,
                  NULL, NULL, VULKAN_TEXTURE_DYNAMIC);
      }
   }

   vk->default_texture = vulkan_create_texture(vk, NULL,
         1, 1, VK_FORMAT_B8G8R8A8_UNORM,
         &zero, NULL, VULKAN_TEXTURE_STATIC);
}

static void vulkan_deinit_textures(vk_t *vk)
{
   int i;
   const void* cached_frame;

   /* Avoid memcpying from a destroyed/unmapped texture later on. */
   video_driver_cached_frame_get(&cached_frame, NULL, NULL, NULL);
   if (vulkan_is_mapped_swapchain_texture_ptr(vk, cached_frame))
      video_driver_set_cached_frame_ptr(NULL);

   vkDestroySampler(vk->context->device, vk->samplers.nearest,        NULL);
   vkDestroySampler(vk->context->device, vk->samplers.linear,         NULL);
   vkDestroySampler(vk->context->device, vk->samplers.mipmap_nearest, NULL);
   vkDestroySampler(vk->context->device, vk->samplers.mipmap_linear,  NULL);

   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      if (vk->swapchain[i].texture.memory != VK_NULL_HANDLE)
         vulkan_destroy_texture(
               vk->context->device, &vk->swapchain[i].texture);

      if (vk->swapchain[i].texture_optimal.memory != VK_NULL_HANDLE)
         vulkan_destroy_texture(
               vk->context->device, &vk->swapchain[i].texture_optimal);
   }

   if (vk->default_texture.memory != VK_NULL_HANDLE)
      vulkan_destroy_texture(vk->context->device, &vk->default_texture);
}

static void vulkan_deinit_command_buffers(vk_t *vk)
{
   int i;
   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      if (vk->swapchain[i].cmd)
         vkFreeCommandBuffers(vk->context->device,
               vk->swapchain[i].cmd_pool, 1, &vk->swapchain[i].cmd);

      vkDestroyCommandPool(vk->context->device,
            vk->swapchain[i].cmd_pool, NULL);
   }
}

static void vulkan_deinit_pipelines(vk_t *vk)
{
   int i;

   vkDestroyPipelineLayout(vk->context->device,
         vk->pipelines.layout, NULL);
   vkDestroyDescriptorSetLayout(vk->context->device,
         vk->pipelines.set_layout, NULL);
   vkDestroyPipeline(vk->context->device,
         vk->pipelines.alpha_blend, NULL);
   vkDestroyPipeline(vk->context->device,
         vk->pipelines.font, NULL);
#ifdef VULKAN_HDR_SWAPCHAIN
   vkDestroyPipeline(vk->context->device,
         vk->pipelines.hdr, NULL);
#endif /* VULKAN_HDR_SWAPCHAIN */

   for (i = 0; i < ARRAY_SIZE(vk->display.pipelines); i++)
      vkDestroyPipeline(vk->context->device,
            vk->display.pipelines[i], NULL);
}

static void vulkan_deinit_framebuffers(vk_t *vk)
{
   int i;
   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      if (vk->backbuffers[i].framebuffer)
         vkDestroyFramebuffer(vk->context->device,
               vk->backbuffers[i].framebuffer, NULL);

      if (vk->backbuffers[i].view)
         vkDestroyImageView(vk->context->device,
               vk->backbuffers[i].view, NULL);
   }

   vkDestroyRenderPass(vk->context->device, vk->render_pass, NULL);
}

#ifdef VULKAN_HDR_SWAPCHAIN
static void vulkan_set_hdr_max_nits(void* data, float max_nits)
{
   vk_t *vk                            = (vk_t*)data;
   vulkan_hdr_uniform_t* mapped_ubo    = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   vk->hdr.max_output_nits             = max_nits;
   mapped_ubo->max_nits                = max_nits;
}

static void vulkan_set_hdr_paper_white_nits(void* data, float paper_white_nits)
{
   vk_t *vk                            = (vk_t*)data;
   vulkan_hdr_uniform_t* mapped_ubo    = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->paper_white_nits = paper_white_nits;
}

static void vulkan_set_hdr_contrast(void* data, float contrast)
{
   vk_t *vk                            = (vk_t*)data;
   vulkan_hdr_uniform_t* mapped_ubo    = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->contrast                = contrast;
}

static void vulkan_set_hdr_expand_gamut(void* data, bool expand_gamut)
{
   vk_t *vk                            = (vk_t*)data;
   vulkan_hdr_uniform_t* mapped_ubo    = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->expand_gamut     = expand_gamut ? 1.0f : 0.0f;
}

static void vulkan_set_hdr_inverse_tonemap(vk_t* vk, bool inverse_tonemap)
{
   vulkan_hdr_uniform_t* mapped_ubo = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->inverse_tonemap      = inverse_tonemap ? 1.0f : 0.0f;
}

static void vulkan_set_hdr10(vk_t* vk, bool hdr10)
{
   vulkan_hdr_uniform_t* mapped_ubo = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->hdr10                = hdr10 ? 1.0f : 0.0f;
}
#endif /* VULKAN_HDR_SWAPCHAIN */

static bool vulkan_init_default_filter_chain(vk_t *vk)
{
   struct vulkan_filter_chain_create_info info;

   if (!vk->context)
      return false;

   info.device                = vk->context->device;
   info.gpu                   = vk->context->gpu;
   info.memory_properties     = &vk->context->memory_properties;
   info.pipeline_cache        = vk->pipelines.cache;
   info.queue                 = vk->context->queue;
   info.command_pool          = vk->swapchain[vk->context->current_frame_index].cmd_pool;
   info.num_passes            = 0;
   info.original_format       = vk->tex_fmt;
   info.max_input_size.width  = vk->tex_w;
   info.max_input_size.height = vk->tex_h;
   info.swapchain.viewport    = vk->vk_vp;
   info.swapchain.format      = vk->context->swapchain_format;
   info.swapchain.render_pass = vk->render_pass;
   info.swapchain.num_indices = vk->context->num_swapchain_images;

   vk->filter_chain           = vulkan_filter_chain_create_default(
         &info,
         vk->video.smooth 
         ? GLSLANG_FILTER_CHAIN_LINEAR 
         : GLSLANG_FILTER_CHAIN_NEAREST);

   if (!vk->filter_chain)
   {
      RARCH_ERR("Failed to create filter chain.\n");
      return false;
   }

#ifdef VULKAN_HDR_SWAPCHAIN
   if (vk->context->flags & VK_CTX_FLAG_HDR_ENABLE)
   {
      struct video_shader* shader_preset = vulkan_filter_chain_get_preset(
      vk->filter_chain); 
      VkFormat rt_format = (shader_preset && shader_preset->passes) ?
         vulkan_filter_chain_get_pass_rt_format(vk->filter_chain, shader_preset->passes - 1) : VK_FORMAT_UNDEFINED;
      bool emits_hdr10 = shader_preset && shader_preset->passes && vulkan_filter_chain_emits_hdr10(vk->filter_chain);

      switch (rt_format)
      {
         case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
            /* If the last shader pass uses a RGB10A2 back buffer 
             * and HDR has been enabled, assume we want to skip 
             * the inverse tonemapper and HDR10 conversion.
             * If we just inherited HDR10 format based on backbuffer,
             * we would have used RGBA8, and thus we should do inverse tonemap as expected. */
            vulkan_set_hdr_inverse_tonemap(vk, !emits_hdr10);
            vulkan_set_hdr10(vk, !emits_hdr10);
            vk->flags |= VK_FLAG_SHOULD_RESIZE;
            break;
         case VK_FORMAT_R16G16B16A16_SFLOAT:
            /* If the last shader pass uses a RGBA16 backbuffer 
             * and HDR has been enabled, assume we want to 
             * skip the inverse tonemapper */
            vulkan_set_hdr_inverse_tonemap(vk, false);
            vulkan_set_hdr10(vk, true);
            vk->flags |= VK_FLAG_SHOULD_RESIZE;
            break;
         case VK_FORMAT_UNDEFINED:
         default:
            vulkan_set_hdr_inverse_tonemap(vk, true);
            vulkan_set_hdr10(vk, true);
            break;
      }
   } 
#endif /* VULKAN_HDR_SWAPCHAIN */

   return true;
}

static bool vulkan_init_filter_chain_preset(vk_t *vk, const char *shader_path)
{
   struct vulkan_filter_chain_create_info info;

   info.device                = vk->context->device;
   info.gpu                   = vk->context->gpu;
   info.memory_properties     = &vk->context->memory_properties;
   info.pipeline_cache        = vk->pipelines.cache;
   info.queue                 = vk->context->queue;
   info.command_pool          = vk->swapchain[vk->context->current_frame_index].cmd_pool;
   info.num_passes            = 0;
   info.original_format       = vk->tex_fmt;
   info.max_input_size.width  = vk->tex_w;
   info.max_input_size.height = vk->tex_h;
   info.swapchain.viewport    = vk->vk_vp;
   info.swapchain.format      = vk->context->swapchain_format;
   info.swapchain.render_pass = vk->render_pass;
   info.swapchain.num_indices = vk->context->num_swapchain_images;

   vk->filter_chain           = vulkan_filter_chain_create_from_preset(
         &info, shader_path,
         vk->video.smooth
         ? GLSLANG_FILTER_CHAIN_LINEAR 
         : GLSLANG_FILTER_CHAIN_NEAREST);

   if (!vk->filter_chain)
   {
      RARCH_ERR("[Vulkan]: Failed to create preset: \"%s\".\n", shader_path);
      return false;
   }

#ifdef VULKAN_HDR_SWAPCHAIN
   if (vk->context->flags & VK_CTX_FLAG_HDR_ENABLE)
   {
      struct video_shader* shader_preset = vulkan_filter_chain_get_preset(vk->filter_chain); 
      VkFormat rt_format = (shader_preset && shader_preset->passes) ? vulkan_filter_chain_get_pass_rt_format(vk->filter_chain, shader_preset->passes - 1) : VK_FORMAT_UNDEFINED;
      bool emits_hdr10 = shader_preset && shader_preset->passes && vulkan_filter_chain_emits_hdr10(vk->filter_chain);

      switch (rt_format)
      {
         case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
            /* If the last shader pass uses a RGB10A2 backbuffer 
             * and HDR has been enabled, assume we want to 
             * skip the inverse tonemapper and HDR10 conversion
             * If we just inherited HDR10 format based on backbuffer,
             * we would have used RGBA8, and thus we should do inverse tonemap as expected. */
            vulkan_set_hdr_inverse_tonemap(vk, !emits_hdr10);
            vulkan_set_hdr10(vk, !emits_hdr10);
            vk->flags |= VK_FLAG_SHOULD_RESIZE;
            break;
         case VK_FORMAT_R16G16B16A16_SFLOAT:
            /* If the last shader pass uses a RGBA16 backbuffer 
             * and HDR has been enabled, assume we want to 
             * skip the inverse tonemapper */
            vulkan_set_hdr_inverse_tonemap(vk, false);
            vulkan_set_hdr10(vk, true);
            vk->flags |= VK_FLAG_SHOULD_RESIZE;
            break;
         case VK_FORMAT_UNDEFINED:
         default:
            vulkan_set_hdr_inverse_tonemap(vk, true);
            vulkan_set_hdr10(vk, true);
            break;
      }
   } 
#endif /* VULKAN_HDR_SWAPCHAIN */

   return true;
}

static bool vulkan_init_filter_chain(vk_t *vk)
{
   const char     *shader_path = retroarch_get_shader_preset();
   enum rarch_shader_type type = video_shader_parse_type(shader_path);

   if (string_is_empty(shader_path))
   {
      RARCH_LOG("[Vulkan]: Loading stock shader.\n");
      return vulkan_init_default_filter_chain(vk);
   }

   if (type != RARCH_SHADER_SLANG)
   {
      RARCH_LOG("[Vulkan]: Only Slang shaders are supported, falling back to stock.\n");
      return vulkan_init_default_filter_chain(vk);
   }

   if (!shader_path || !vulkan_init_filter_chain_preset(vk, shader_path))
      vulkan_init_default_filter_chain(vk);

   return true;
}

static void vulkan_init_static_resources(vk_t *vk)
{
   int i;
   uint32_t blank[4 * 4];
   VkCommandPoolCreateInfo pool_info = {
      VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };

   /* Create the pipeline cache. */
   VkPipelineCacheCreateInfo cache   = {
      VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO };

   pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

   if (!vk->context)
      return;

   vkCreatePipelineCache(vk->context->device,
         &cache, NULL, &vk->pipelines.cache);

   pool_info.queueFamilyIndex = vk->context->graphics_queue_index;

   vkCreateCommandPool(vk->context->device,
         &pool_info, NULL, &vk->staging_pool);

   for (i = 0; i < 4 * 4; i++)
      blank[i] = -1u;

   vk->display.blank_texture = vulkan_create_texture(vk, NULL,
         4, 4, VK_FORMAT_B8G8R8A8_UNORM,
         blank, NULL, VULKAN_TEXTURE_STATIC);
}

static void vulkan_deinit_static_resources(vk_t *vk)
{
   int i;
   vkDestroyPipelineCache(vk->context->device,
         vk->pipelines.cache, NULL);
   vulkan_destroy_texture(
         vk->context->device,
         &vk->display.blank_texture);

   vkDestroyCommandPool(vk->context->device,
         vk->staging_pool, NULL);
   free(vk->hw.cmd);
   free(vk->hw.wait_dst_stages);
   free(vk->hw.semaphores);

   for (i = 0; i < VULKAN_MAX_SWAPCHAIN_IMAGES; i++)
      if (vk->readback.staging[i].memory != VK_NULL_HANDLE)
         vulkan_destroy_texture(
               vk->context->device,
               &vk->readback.staging[i]);
}

static void vulkan_deinit_menu(vk_t *vk)
{
   int i;
   for (i = 0; i < VULKAN_MAX_SWAPCHAIN_IMAGES; i++)
   {
      if (vk->menu.textures[i].memory)
         vulkan_destroy_texture(
               vk->context->device, &vk->menu.textures[i]);
      if (vk->menu.textures_optimal[i].memory)
         vulkan_destroy_texture(
               vk->context->device, &vk->menu.textures_optimal[i]);
   }
}

#ifdef VULKAN_HDR_SWAPCHAIN
static void vulkan_destroy_hdr_buffer(VkDevice device, struct vk_image *img)
{
   vkDestroyImageView(device, img->view, NULL);
   vkDestroyImage(device, img->image, NULL);
   vkDestroyFramebuffer(device, img->framebuffer, NULL);
   vkFreeMemory(device, img->memory, NULL);
   memset(img, 0, sizeof(*img));
}
#endif

static void vulkan_free(void *data)
{
   vk_t *vk = (vk_t*)data;
   if (!vk)
      return;

   if (vk->context && vk->context->device)
   {
#ifdef HAVE_THREADS
      slock_lock(vk->context->queue_lock);
#endif
      vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
      slock_unlock(vk->context->queue_lock);
#endif
      vulkan_deinit_pipelines(vk);
      vulkan_deinit_framebuffers(vk);
      vulkan_deinit_descriptor_pool(vk);
      vulkan_deinit_textures(vk);
      vulkan_deinit_buffers(vk);
      vulkan_deinit_command_buffers(vk);

      /* No need to init this since textures are create on-demand. */
      vulkan_deinit_menu(vk);

      font_driver_free_osd();

      vulkan_deinit_static_resources(vk);
#ifdef HAVE_OVERLAY
      vulkan_overlay_free(vk);
#endif

      if (vk->filter_chain)
         vulkan_filter_chain_free((vulkan_filter_chain_t*)vk->filter_chain);

#ifdef VULKAN_HDR_SWAPCHAIN
      vulkan_destroy_buffer(vk->context->device, &vk->hdr.ubo);
      vulkan_destroy_hdr_buffer(vk->context->device, &vk->main_buffer);
      video_driver_unset_hdr_support();
#endif /* VULKAN_HDR_SWAPCHAIN */

      if (vk->ctx_driver && vk->ctx_driver->destroy)
         vk->ctx_driver->destroy(vk->ctx_data);
      video_context_driver_free();
   }

   scaler_ctx_gen_reset(&vk->readback.scaler_bgr);
   scaler_ctx_gen_reset(&vk->readback.scaler_rgb);
   free(vk);
}

static uint32_t vulkan_get_sync_index(void *handle)
{
   vk_t *vk = (vk_t*)handle;
   return vk->context->current_frame_index;
}

static uint32_t vulkan_get_sync_index_mask(void *handle)
{
   vk_t *vk = (vk_t*)handle;
   return (1 << vk->context->num_swapchain_images) - 1;
}

static void vulkan_set_image(void *handle,
      const struct retro_vulkan_image *image,
      uint32_t num_semaphores,
      const VkSemaphore *semaphores,
      uint32_t src_queue_family)
{
   vk_t *vk              = (vk_t*)handle;

   vk->hw.image          = image;
   vk->hw.num_semaphores = num_semaphores;

   if (num_semaphores > 0)
   {
      int i;

      /* Allocate one extra in case we need to use WSI acquire semaphores. */
      VkPipelineStageFlags *stage_flags = (VkPipelineStageFlags*)realloc(vk->hw.wait_dst_stages,
            sizeof(VkPipelineStageFlags) * (vk->hw.num_semaphores + 1));

      VkSemaphore *new_semaphores = (VkSemaphore*)realloc(vk->hw.semaphores,
            sizeof(VkSemaphore) * (vk->hw.num_semaphores + 1));

      /* If this fails, we're screwed anyways. */
      retro_assert(stage_flags && new_semaphores);

      vk->hw.wait_dst_stages = stage_flags;
      vk->hw.semaphores = new_semaphores;

      for (i = 0; i < vk->hw.num_semaphores; i++)
      {
         vk->hw.wait_dst_stages[i] = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
         vk->hw.semaphores[i]      = semaphores[i];
      }

      vk->flags                   |= VK_FLAG_HW_VALID_SEMAPHORE;
      vk->hw.src_queue_family      = src_queue_family;
   }
}

static void vulkan_wait_sync_index(void *handle)
{
   (void)handle;
   /* no-op. RetroArch already waits for this
    * in gfx_ctx_swap_buffers(). */
}

static void vulkan_set_command_buffers(void *handle, uint32_t num_cmd,
      const VkCommandBuffer *cmd)
{
   vk_t *vk                   = (vk_t*)handle;
   unsigned required_capacity = num_cmd + 1;
   if (required_capacity > vk->hw.capacity_cmd)
   {
      VkCommandBuffer *hw_cmd = (VkCommandBuffer*)
         realloc(vk->hw.cmd,
            sizeof(VkCommandBuffer) * required_capacity);

      /* If this fails, we're just screwed. */
      retro_assert(hw_cmd);

      vk->hw.cmd          = hw_cmd;
      vk->hw.capacity_cmd = required_capacity;
   }

   vk->hw.num_cmd = num_cmd;
   memcpy(vk->hw.cmd, cmd, sizeof(VkCommandBuffer) * num_cmd);
}

static void vulkan_lock_queue(void *handle)
{
   vk_t *vk = (vk_t*)handle;
#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
}

static void vulkan_unlock_queue(void *handle)
{
   vk_t *vk = (vk_t*)handle;
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif
}

static void vulkan_set_signal_semaphore(void *handle, VkSemaphore semaphore)
{
   vk_t *vk = (vk_t*)handle;
   vk->hw.signal_semaphore = semaphore;
}

static void vulkan_init_hw_render(vk_t *vk)
{
   struct retro_hw_render_interface_vulkan *iface   =
      &vk->hw.iface;
   struct retro_hw_render_callback *hwr =
      video_driver_get_hw_context();

   if (hwr->context_type != RETRO_HW_CONTEXT_VULKAN)
      return;

   vk->flags                  |= VK_FLAG_HW_ENABLE;

   iface->interface_type       = RETRO_HW_RENDER_INTERFACE_VULKAN;
   iface->interface_version    = RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION;
   iface->instance             = vk->context->instance;
   iface->gpu                  = vk->context->gpu;
   iface->device               = vk->context->device;

   iface->queue                = vk->context->queue;
   iface->queue_index          = vk->context->graphics_queue_index;

   iface->handle               = vk;
   iface->set_image            = vulkan_set_image;
   iface->get_sync_index       = vulkan_get_sync_index;
   iface->get_sync_index_mask  = vulkan_get_sync_index_mask;
   iface->wait_sync_index      = vulkan_wait_sync_index;
   iface->set_command_buffers  = vulkan_set_command_buffers;
   iface->lock_queue           = vulkan_lock_queue;
   iface->unlock_queue         = vulkan_unlock_queue;
   iface->set_signal_semaphore = vulkan_set_signal_semaphore;

   iface->get_device_proc_addr   = vkGetDeviceProcAddr;
   iface->get_instance_proc_addr = vulkan_symbol_wrapper_instance_proc_addr();
}

static void vulkan_init_readback(vk_t *vk, settings_t *settings)
{
   /* Only bother with this if we're doing GPU recording.
    * Check recording_st->enable and not
    * driver.recording_data, because recording is
    * not initialized yet.
    */
   recording_state_t 
      *recording_st        = recording_state_get_ptr();
   bool recording_enabled  = recording_st->enable;
   bool video_gpu_record   = settings->bools.video_gpu_record;

   if (!(video_gpu_record && recording_enabled))
   {
      vk->flags                       &= ~VK_FLAG_READBACK_STREAMED;
      return;
   }

   vk->flags                          |=  VK_FLAG_READBACK_STREAMED;

   vk->readback.scaler_bgr.in_width    = vk->vp.width;
   vk->readback.scaler_bgr.in_height   = vk->vp.height;
   vk->readback.scaler_bgr.out_width   = vk->vp.width;
   vk->readback.scaler_bgr.out_height  = vk->vp.height;
   vk->readback.scaler_bgr.in_fmt      = SCALER_FMT_ARGB8888;
   vk->readback.scaler_bgr.out_fmt     = SCALER_FMT_BGR24;
   vk->readback.scaler_bgr.scaler_type = SCALER_TYPE_POINT;

   vk->readback.scaler_rgb.in_width    = vk->vp.width;
   vk->readback.scaler_rgb.in_height   = vk->vp.height;
   vk->readback.scaler_rgb.out_width   = vk->vp.width;
   vk->readback.scaler_rgb.out_height  = vk->vp.height;
   vk->readback.scaler_rgb.in_fmt      = SCALER_FMT_ABGR8888;
   vk->readback.scaler_rgb.out_fmt     = SCALER_FMT_BGR24;
   vk->readback.scaler_rgb.scaler_type = SCALER_TYPE_POINT;

   if (!scaler_ctx_gen_filter(&vk->readback.scaler_bgr))
   {
      vk->flags &= ~VK_FLAG_READBACK_STREAMED;
      RARCH_ERR("[Vulkan]: Failed to initialize scaler context.\n");
   }

   if (!scaler_ctx_gen_filter(&vk->readback.scaler_rgb))
   {
      vk->flags &= ~VK_FLAG_READBACK_STREAMED;
      RARCH_ERR("[Vulkan]: Failed to initialize scaler context.\n");
   }
}

static void *vulkan_init(const video_info_t *video,
      input_driver_t **input,
      void **input_data)
{
   unsigned full_x, full_y;
   unsigned win_width;
   unsigned win_height;
   unsigned mode_width                = 0;
   unsigned mode_height               = 0;
   int interval                       = 0;
   unsigned temp_width                = 0;
   unsigned temp_height               = 0;
   const gfx_ctx_driver_t *ctx_driver = NULL;
   settings_t *settings               = config_get_ptr();
#ifdef VULKAN_HDR_SWAPCHAIN
   vulkan_hdr_uniform_t* mapped_ubo   = NULL;
#endif
   vk_t *vk                           = (vk_t*)calloc(1, sizeof(*vk));
   if (!vk)
      return NULL;
   ctx_driver                         = vulkan_get_context(vk, settings);
   if (!ctx_driver)
   {
      RARCH_ERR("[Vulkan]: Failed to get Vulkan context.\n");
      goto error;
   }

#ifdef VULKAN_HDR_SWAPCHAIN
   vk->hdr.max_output_nits             = settings->floats.video_hdr_max_nits;
   vk->hdr.min_output_nits             = 0.001f;
   vk->hdr.max_cll                     = 0.0f;
   vk->hdr.max_fall                    = 0.0f;
#endif /* VULKAN_HDR_SWAPCHAIN */

   vk->video                          = *video;
   vk->ctx_driver                     = ctx_driver;

   video_context_driver_set((const gfx_ctx_driver_t*)ctx_driver);
   
   RARCH_LOG("[Vulkan]: Found vulkan context: \"%s\".\n", ctx_driver->ident);

   if (vk->ctx_driver->get_video_size)
      vk->ctx_driver->get_video_size(vk->ctx_data,
            &mode_width, &mode_height);

   full_x                             = mode_width;
   full_y                             = mode_height;
   mode_width                         = 0;
   mode_height                        = 0;

   RARCH_LOG("[Vulkan]: Detecting screen resolution: %ux%u.\n", full_x, full_y);
   interval = video->vsync ? video->swap_interval : 0;

   if (ctx_driver->swap_interval)
   {
      bool adaptive_vsync_enabled            = video_driver_test_all_flags(
            GFX_CTX_FLAGS_ADAPTIVE_VSYNC) && video->adaptive_vsync;
      if (adaptive_vsync_enabled && interval == 1)
         interval = -1;
      ctx_driver->swap_interval(vk->ctx_data, interval);
   }

   win_width  = video->width;
   win_height = video->height;

   if (video->fullscreen && (win_width == 0) && (win_height == 0))
   {
      win_width  = full_x;
      win_height = full_y;
   }

   if (     !vk->ctx_driver->set_video_mode
         || !vk->ctx_driver->set_video_mode(vk->ctx_data,
            win_width, win_height, video->fullscreen))
   {
      RARCH_ERR("[Vulkan]: Failed to set video mode.\n");
      goto error;
   }

   if (vk->ctx_driver->get_video_size)
      vk->ctx_driver->get_video_size(vk->ctx_data,
            &mode_width, &mode_height);

   temp_width  = mode_width;
   temp_height = mode_height;

   if (temp_width != 0 && temp_height != 0)
      video_driver_set_size(temp_width, temp_height);
   video_driver_get_size(&temp_width, &temp_height);
   vk->video_width       = temp_width;
   vk->video_height      = temp_height;

   RARCH_LOG("[Vulkan]: Using resolution %ux%u.\n", temp_width, temp_height);

   if (!vk->ctx_driver || !vk->ctx_driver->get_context_data)
   {
      RARCH_ERR("[Vulkan]: Failed to get context data.\n");
      goto error;
   }

   *(void**)&vk->context = vk->ctx_driver->get_context_data(vk->ctx_data);

   if (video->vsync)
      vk->flags         |=  VK_FLAG_VSYNC;
   else
      vk->flags         &= ~VK_FLAG_VSYNC;
   if (video->fullscreen)
      vk->flags         |=  VK_FLAG_FULLSCREEN;
   else
      vk->flags         &= ~VK_FLAG_FULLSCREEN;
   vk->tex_w             = RARCH_SCALE_BASE * video->input_scale;
   vk->tex_h             = RARCH_SCALE_BASE * video->input_scale;
   vk->tex_fmt           = video->rgb32
      ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R5G6B5_UNORM_PACK16;
   if (video->force_aspect)
      vk->flags         |=  VK_FLAG_KEEP_ASPECT;
   else
      vk->flags         &= ~VK_FLAG_KEEP_ASPECT;
   RARCH_LOG("[Vulkan]: Using %s format.\n", video->rgb32 ? "BGRA8888" : "RGB565");

   /* Set the viewport to fix recording, since it needs to know
    * the viewport sizes before we start running. */
   vulkan_set_viewport(vk, temp_width, temp_height, false, true);

#ifdef VULKAN_HDR_SWAPCHAIN
   vk->hdr.ubo                  = vulkan_create_buffer(vk->context, sizeof(vulkan_hdr_uniform_t), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);

   mapped_ubo                   = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

   mapped_ubo->mvp              = vk->mvp_no_rot; 
   mapped_ubo->max_nits         = settings->floats.video_hdr_max_nits;
   mapped_ubo->paper_white_nits = settings->floats.video_hdr_paper_white_nits;
   mapped_ubo->contrast         = VIDEO_HDR_MAX_CONTRAST - settings->floats.video_hdr_display_contrast;
   mapped_ubo->expand_gamut     = settings->bools.video_hdr_expand_gamut;
   mapped_ubo->inverse_tonemap  = 1.0f;     /* Use this to turn on/off the inverse tonemap */
   mapped_ubo->hdr10            = 1.0f;     /* Use this to turn on/off the hdr10 */
#endif /* VULKAN_HDR_SWAPCHAIN */

   vulkan_init_hw_render(vk);
   vulkan_init_static_resources(vk);
   if (vk->context)
   {
      int i;
      static const VkDescriptorPoolSize pool_sizes[2] = {
         { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VULKAN_DESCRIPTOR_MANAGER_BLOCK_SETS },
         { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VULKAN_DESCRIPTOR_MANAGER_BLOCK_SETS * 2 },
      };
      vk->num_swapchain_images = vk->context->num_swapchain_images;

      vulkan_init_render_pass(vk);
      vulkan_init_framebuffers(vk);
      vulkan_init_pipelines(vk);
      vulkan_init_samplers(vk);
      vulkan_init_textures(vk);

      for (i = 0; i < vk->num_swapchain_images; i++)
      {
         VkCommandPoolCreateInfo pool_info;
         VkCommandBufferAllocateInfo info;

         vk->swapchain[i].descriptor_manager =
            vulkan_create_descriptor_manager(
                  vk->context->device,
                  pool_sizes, 2, vk->pipelines.set_layout);
         vk->swapchain[i].vbo                = 
            vulkan_buffer_chain_init(
               VULKAN_BUFFER_BLOCK_SIZE, 16,
               VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
         vk->swapchain[i].ubo                = 
            vulkan_buffer_chain_init(
               VULKAN_BUFFER_BLOCK_SIZE,
               vk->context->gpu_properties.limits.minUniformBufferOffsetAlignment,
               VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);

         pool_info.sType            =
            VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
         pool_info.pNext            = NULL;
         /* RESET_COMMAND_BUFFER_BIT allows command buffer to be reset. */
         pool_info.flags            =
            VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
         pool_info.queueFamilyIndex = vk->context->graphics_queue_index;

         vkCreateCommandPool(vk->context->device,
               &pool_info, NULL, &vk->swapchain[i].cmd_pool);

         info.sType                 =
            VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
         info.pNext                 = NULL;
         info.commandPool           = vk->swapchain[i].cmd_pool;
         info.level                 = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
         info.commandBufferCount    = 1;

         vkAllocateCommandBuffers(vk->context->device,
               &info, &vk->swapchain[i].cmd);
      }
   }

   if (!vulkan_init_filter_chain(vk))
   {
      RARCH_ERR("[Vulkan]: Failed to init filter chain.\n");
      goto error;
   }

   if (vk->ctx_driver->input_driver)
   {
      const char *joypad_name = settings->arrays.input_joypad_driver;
      vk->ctx_driver->input_driver(
            vk->ctx_data, joypad_name,
            input, input_data);
   }

   if (video->font_enable)
      font_driver_init_osd(vk,
            video,
            false,
            video->is_threaded,
            FONT_DRIVER_RENDER_VULKAN_API);

   vulkan_init_readback(vk, settings);
   return vk;

error:
   vulkan_free(vk);
   return NULL;
}

static void vulkan_check_swapchain(vk_t *vk)
{
   struct vulkan_filter_chain_swapchain_info filter_info;

#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
   vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif
   vulkan_deinit_pipelines(vk);
   vulkan_deinit_framebuffers(vk);
   vulkan_deinit_descriptor_pool(vk);
   vulkan_deinit_textures(vk);
   vulkan_deinit_buffers(vk);
   vulkan_deinit_command_buffers(vk);
   if (vk->context)
   {
      int i;
      static const VkDescriptorPoolSize pool_sizes[2] = {
         { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VULKAN_DESCRIPTOR_MANAGER_BLOCK_SETS },
         { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VULKAN_DESCRIPTOR_MANAGER_BLOCK_SETS * 2 },
      };
      vk->num_swapchain_images = vk->context->num_swapchain_images;

      vulkan_init_render_pass(vk);
      vulkan_init_framebuffers(vk);
      vulkan_init_pipelines(vk);
      vulkan_init_samplers(vk);
      vulkan_init_textures(vk);

      for (i = 0; i < vk->num_swapchain_images; i++)
      {
         VkCommandPoolCreateInfo pool_info;
         VkCommandBufferAllocateInfo info;

         vk->swapchain[i].descriptor_manager =
            vulkan_create_descriptor_manager(
                  vk->context->device,
                  pool_sizes, 2, vk->pipelines.set_layout);

         vk->swapchain[i].vbo       = vulkan_buffer_chain_init(
               VULKAN_BUFFER_BLOCK_SIZE,
               16,
               VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
         vk->swapchain[i].ubo       = vulkan_buffer_chain_init(
               VULKAN_BUFFER_BLOCK_SIZE,
               vk->context->gpu_properties.limits.minUniformBufferOffsetAlignment,
               VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);

         pool_info.sType            =
            VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
         pool_info.pNext            = NULL;
         /* RESET_COMMAND_BUFFER_BIT allows command buffer to be reset. */
         pool_info.flags            =
            VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
         pool_info.queueFamilyIndex = vk->context->graphics_queue_index;

         vkCreateCommandPool(vk->context->device,
               &pool_info, NULL, &vk->swapchain[i].cmd_pool);

         info.sType                 =
            VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
         info.pNext                 = NULL;
         info.commandPool           = vk->swapchain[i].cmd_pool;
         info.level                 = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
         info.commandBufferCount    = 1;

         vkAllocateCommandBuffers(vk->context->device,
               &info, &vk->swapchain[i].cmd);
      }
   }
   vk->context->flags              &= ~VK_CTX_FLAG_INVALID_SWAPCHAIN;

   filter_info.viewport             = vk->vk_vp;
   filter_info.format               = vk->context->swapchain_format;
   filter_info.render_pass          = vk->render_pass;
   filter_info.num_indices          = vk->context->num_swapchain_images;
   if (
       !vulkan_filter_chain_update_swapchain_info(
          (vulkan_filter_chain_t*)vk->filter_chain,
          &filter_info)
      )
      RARCH_ERR("Failed to update filter chain info. This will probably lead to a crash ...\n");
}

static void vulkan_set_nonblock_state(void *data, bool state,
      bool adaptive_vsync_enabled,
      unsigned swap_interval)
{
   vk_t *vk                    = (vk_t*)data;

   if (!vk)
      return;

   if (vk->ctx_driver->swap_interval)
   {
      int interval             = 0;
      if (!state)
         interval = swap_interval;
      if (adaptive_vsync_enabled && interval == 1)
         interval = -1;
      vk->ctx_driver->swap_interval(vk->ctx_data, interval);
   }

   /* Changing vsync might require recreating the swapchain,
    * which means new VkImages to render into. */
   if (vk->context->flags & VK_CTX_FLAG_INVALID_SWAPCHAIN)
      vulkan_check_swapchain(vk);
}

static bool vulkan_alive(void *data)
{
   bool ret             = false;
   bool quit            = false;
   bool resize          = false;
   vk_t *vk             = (vk_t*)data;
   unsigned temp_width  = vk->video_width;
   unsigned temp_height = vk->video_height;

   vk->ctx_driver->check_window(vk->ctx_data,
            &quit, &resize, &temp_width, &temp_height);

   if (quit)
      vk->flags |= VK_FLAG_QUITTING;
   else if (resize)
      vk->flags |= VK_FLAG_SHOULD_RESIZE;

   ret = (!(vk->flags & VK_FLAG_QUITTING));

   if (temp_width != 0 && temp_height != 0)
   {
      video_driver_set_size(temp_width, temp_height);
      vk->video_width  = temp_width;
      vk->video_height = temp_height;
   }

   return ret;
}

static bool vulkan_suppress_screensaver(void *data, bool enable)
{
   bool enabled = enable;
   vk_t *vk     = (vk_t*)data;

   if (vk->ctx_data && vk->ctx_driver->suppress_screensaver)
      return vk->ctx_driver->suppress_screensaver(vk->ctx_data, enabled);
   return false;
}

static bool vulkan_set_shader(void *data,
      enum rarch_shader_type type, const char *path)
{
   vk_t *vk = (vk_t*)data;
   if (!vk)
      return false;

   if (vk->filter_chain)
      vulkan_filter_chain_free((vulkan_filter_chain_t*)vk->filter_chain);
   vk->filter_chain = NULL;

   if (!string_is_empty(path) && type != RARCH_SHADER_SLANG)
   {
      RARCH_WARN("[Vulkan]: Only Slang shaders are supported. Falling back to stock.\n");
      path = NULL;
   }

   if (string_is_empty(path))
   {
      vulkan_init_default_filter_chain(vk);
      return true;
   }

   if (!vulkan_init_filter_chain_preset(vk, path))
   {
      RARCH_ERR("[Vulkan]: Failed to create filter chain: \"%s\". Falling back to stock.\n", path);
      vulkan_init_default_filter_chain(vk);
      return false;
   }

   return true;
}

static void vulkan_set_projection(vk_t *vk,
      struct video_ortho *ortho, bool allow_rotate)
{
   float radians, cosine, sine;
   static math_matrix_4x4 rot     = {
      {  0.0f,     0.0f,    0.0f,    0.0f ,
         0.0f,     0.0f,    0.0f,    0.0f ,
         0.0f,     0.0f,    0.0f,    0.0f ,
         0.0f,     0.0f,    0.0f,    1.0f }
   };

   /* Calculate projection. */
   matrix_4x4_ortho(vk->mvp_no_rot, ortho->left, ortho->right,
         ortho->bottom, ortho->top, ortho->znear, ortho->zfar);

   if (!allow_rotate)
   {
      vk->mvp = vk->mvp_no_rot;
      return;
   }

   radians                 = M_PI * vk->rotation / 180.0f;
   cosine                  = cosf(radians);
   sine                    = sinf(radians);
   MAT_ELEM_4X4(rot, 0, 0) = cosine;
   MAT_ELEM_4X4(rot, 0, 1) = -sine;
   MAT_ELEM_4X4(rot, 1, 0) = sine;
   MAT_ELEM_4X4(rot, 1, 1) = cosine;
   matrix_4x4_multiply(vk->mvp, rot, vk->mvp_no_rot);
}

static void vulkan_set_rotation(void *data, unsigned rotation)
{
   vk_t *vk               = (vk_t*)data;
   struct video_ortho ortho = {0, 1, 0, 1, -1, 1};

   if (!vk)
      return;

   vk->rotation = 270 * rotation;
   vulkan_set_projection(vk, &ortho, true);
}

static void vulkan_set_video_mode(void *data,
      unsigned width, unsigned height,
      bool fullscreen)
{
   vk_t *vk               = (vk_t*)data;
   if (vk->ctx_driver->set_video_mode)
      vk->ctx_driver->set_video_mode(vk->ctx_data,
            width, height, fullscreen);
}

static void vulkan_set_viewport(void *data, unsigned viewport_width,
      unsigned viewport_height, bool force_full, bool allow_rotate)
{
   int x                     = 0;
   int y                     = 0;
   float device_aspect       = (float)viewport_width / viewport_height;
   struct video_ortho ortho  = {0, 1, 0, 1, -1, 1};
   settings_t *settings      = config_get_ptr();
   bool video_scale_integer  = settings->bools.video_scale_integer;
   unsigned aspect_ratio_idx = settings->uints.video_aspect_ratio_idx;
   vk_t *vk                  = (vk_t*)data;
   unsigned width            = vk->video_width;
   unsigned height           = vk->video_height;

   if (vk->ctx_driver->translate_aspect)
      device_aspect         = vk->ctx_driver->translate_aspect(
            vk->ctx_data, viewport_width, viewport_height);

   if (video_scale_integer && !force_full)
   {
      video_viewport_get_scaled_integer(&vk->vp,
            viewport_width, viewport_height,
            video_driver_get_aspect_ratio(),
            vk->flags & VK_FLAG_KEEP_ASPECT);
      viewport_width  = vk->vp.width;
      viewport_height = vk->vp.height;
   }
   else if ((vk->flags & VK_FLAG_KEEP_ASPECT) && !force_full)
   {
      float desired_aspect = video_driver_get_aspect_ratio();

#if defined(HAVE_MENU)
      if (aspect_ratio_idx == ASPECT_RATIO_CUSTOM)
      {
         const struct video_viewport *custom = video_viewport_get_custom();

         /* Vulkan has top-left origin viewport. */
         x               = custom->x;
         y               = custom->y;
         viewport_width  = custom->width;
         viewport_height = custom->height;
      }
      else
#endif
      {
         float delta;

         if (fabsf(device_aspect - desired_aspect) < 0.0001f)
         {
            /* If the aspect ratios of screen and desired aspect
             * ratio are sufficiently equal (floating point stuff),
             * assume they are actually equal.
             */
         }
         else if (device_aspect > desired_aspect)
         {
            delta          = (desired_aspect / device_aspect - 1.0f)
               / 2.0f + 0.5f;
            x              = (int)roundf(viewport_width * (0.5f - delta));
            viewport_width = (unsigned)roundf(2.0f * viewport_width * delta);
         }
         else
         {
            delta           = (device_aspect / desired_aspect - 1.0f)
               / 2.0f + 0.5f;
            y               = (int)roundf(viewport_height * (0.5f - delta));
            viewport_height = (unsigned)roundf(2.0f * viewport_height * delta);
         }
      }

      vk->vp.x      = x;
      vk->vp.y      = y;
      vk->vp.width  = viewport_width;
      vk->vp.height = viewport_height;
   }
   else
   {
      vk->vp.x      = 0;
      vk->vp.y      = 0;
      vk->vp.width  = viewport_width;
      vk->vp.height = viewport_height;
   }

#if defined(RARCH_MOBILE)
   /* In portrait mode, we want viewport to gravitate to top of screen. */
   if (device_aspect < 1.0f)
      vk->vp.y = 0;
#endif

   vulkan_set_projection(vk, &ortho, allow_rotate);

   /* Set last backbuffer viewport. */
   if (!force_full)
   {
      vk->vp_out_width  = viewport_width;
      vk->vp_out_height = viewport_height;
   }

   vk->vk_vp.x          = (float)vk->vp.x;
   vk->vk_vp.y          = (float)vk->vp.y;
   vk->vk_vp.width      = (float)vk->vp.width;
   vk->vk_vp.height     = (float)vk->vp.height;
   vk->vk_vp.minDepth   = 0.0f;
   vk->vk_vp.maxDepth   = 1.0f;

   vk->tracker.dirty |= VULKAN_DIRTY_DYNAMIC_BIT;
}

static void vulkan_readback(vk_t *vk)
{
   VkBufferImageCopy region;
   struct vk_texture *staging;
   struct video_viewport vp;
   VkMemoryBarrier barrier;

   vp.x                                   = 0;
   vp.y                                   = 0;
   vp.width                               = 0;
   vp.height                              = 0;
   vp.full_width                          = 0;
   vp.full_height                         = 0;

   vulkan_viewport_info(vk, &vp);

   region.bufferOffset                    = 0;
   region.bufferRowLength                 = 0;
   region.bufferImageHeight               = 0;
   region.imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
   region.imageSubresource.mipLevel       = 0;
   region.imageSubresource.baseArrayLayer = 0;
   region.imageSubresource.layerCount     = 1;
   region.imageOffset.x                   = vp.x;
   region.imageOffset.y                   = vp.y;
   region.imageOffset.z                   = 0;
   region.imageExtent.width               = vp.width;
   region.imageExtent.height              = vp.height;
   region.imageExtent.depth               = 1;

   staging  = &vk->readback.staging[vk->context->current_frame_index];
   *staging = vulkan_create_texture(vk,
         staging->memory != VK_NULL_HANDLE ? staging : NULL,
         vk->vp.width, vk->vp.height,
         VK_FORMAT_B8G8R8A8_UNORM, /* Formats don't matter for readback since it's a raw copy. */
         NULL, NULL, VULKAN_TEXTURE_READBACK);

   vkCmdCopyImageToBuffer(vk->cmd, vk->backbuffer->image,
         VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
         staging->buffer,
         1, &region);

   /* Make the data visible to host. */
   barrier.sType         = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
   barrier.pNext         = NULL;
   barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
   barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
   vkCmdPipelineBarrier(vk->cmd,
         VK_PIPELINE_STAGE_TRANSFER_BIT,
         VK_PIPELINE_STAGE_HOST_BIT, 0,
         1, &barrier, 0, NULL, 0, NULL);
}

static void vulkan_inject_black_frame(vk_t *vk, video_frame_info_t *video_info)
{
   VkCommandBufferBeginInfo begin_info = {
      VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
   VkSubmitInfo submit_info            = {
      VK_STRUCTURE_TYPE_SUBMIT_INFO };

   const VkClearColorValue clear_color = {{ 0.0f, 0.0f, 0.0f, 1.0f }};
   const VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
   unsigned frame_index                = vk->context->current_frame_index;
   unsigned swapchain_index            = vk->context->current_swapchain_index;
   struct vk_per_frame *chain          = &vk->swapchain[frame_index];
   struct vk_image *backbuffer         = &vk->backbuffers[swapchain_index];
   vk->chain                           = chain;
   vk->cmd                             = chain->cmd;
   begin_info.flags                    = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
   vkResetCommandBuffer(vk->cmd, 0);
   vkBeginCommandBuffer(vk->cmd, &begin_info);

   VULKAN_IMAGE_LAYOUT_TRANSITION(vk->cmd, backbuffer->image,
         VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
         0, VK_ACCESS_TRANSFER_WRITE_BIT,
         VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
         VK_PIPELINE_STAGE_TRANSFER_BIT);

   vkCmdClearColorImage(vk->cmd, backbuffer->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
         &clear_color, 1, &range);

   VULKAN_IMAGE_LAYOUT_TRANSITION(vk->cmd, backbuffer->image,
         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
         VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT,
         VK_PIPELINE_STAGE_TRANSFER_BIT,
         VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);

   vkEndCommandBuffer(vk->cmd);

   submit_info.commandBufferCount      = 1;
   submit_info.pCommandBuffers         = &vk->cmd;
   if (
            (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
         && (vk->context->swapchain_semaphores[swapchain_index] !=
            VK_NULL_HANDLE))
   {
      submit_info.signalSemaphoreCount = 1;
      submit_info.pSignalSemaphores    = &vk->context->swapchain_semaphores[swapchain_index];
   }

   if (     (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
         && (vk->context->swapchain_acquire_semaphore != VK_NULL_HANDLE))
   {
      static const VkPipelineStageFlags wait_stage        =
         VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;

      assert(!vk->context->swapchain_wait_semaphores[frame_index]);
      vk->context->swapchain_wait_semaphores[frame_index] =
         vk->context->swapchain_acquire_semaphore;
      vk->context->swapchain_acquire_semaphore            = VK_NULL_HANDLE;
      submit_info.waitSemaphoreCount                      = 1;
      submit_info.pWaitSemaphores                         = &vk->context->swapchain_wait_semaphores[frame_index];
      submit_info.pWaitDstStageMask                       = &wait_stage;
   }

#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
   vkQueueSubmit(vk->context->queue, 1,
         &submit_info, vk->context->swapchain_fences[frame_index]);
   vk->context->swapchain_fences_signalled[frame_index] = true;
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif
}

static bool vulkan_frame(void *data, const void *frame,
      unsigned frame_width, unsigned frame_height,
      uint64_t frame_count,
      unsigned pitch, const char *msg, video_frame_info_t *video_info)
{
   int i;
   VkSubmitInfo submit_info;
   VkClearValue clear_color;
   VkRenderPassBeginInfo rp_info;
   VkCommandBufferBeginInfo begin_info;
   VkSemaphore signal_semaphores[2];
   vk_t *vk                                      = (vk_t*)data;
   bool waits_for_semaphores                     = false;
   unsigned width                                = video_info->width;
   unsigned height                               = video_info->height;
   bool statistics_show                          = video_info->statistics_show;
   const char *stat_text                         = video_info->stat_text;
   unsigned black_frame_insertion                = video_info->black_frame_insertion;
   bool input_driver_nonblock_state              = video_info->input_driver_nonblock_state;
   bool runloop_is_slowmotion                    = video_info->runloop_is_slowmotion;
   bool runloop_is_paused                        = video_info->runloop_is_paused;
   unsigned video_width                          = video_info->width;
   unsigned video_height                         = video_info->height;
   struct font_params *osd_params                = (struct font_params*)
      &video_info->osd_stat_params;
#ifdef HAVE_MENU
   bool menu_is_alive                            = video_info->menu_is_alive;
#endif
#ifdef HAVE_GFX_WIDGETS
   bool widgets_active                           = video_info->widgets_active;
#endif
   unsigned frame_index                          =
      vk->context->current_frame_index;
   unsigned swapchain_index                      =
      vk->context->current_swapchain_index;
   bool overlay_behind_menu                      = video_info->overlay_behind_menu;

#ifdef VULKAN_HDR_SWAPCHAIN
   bool use_main_buffer                          = (vk->context->flags &
         VK_CTX_FLAG_HDR_ENABLE)
      && (!vk->filter_chain || !vulkan_filter_chain_emits_hdr10(vk->filter_chain));
#endif /* VULKAN_HDR_SWAPCHAIN */

   /* Bookkeeping on start of frame. */
   struct vk_per_frame *chain                    = &vk->swapchain[frame_index];
   struct vk_image *backbuffer                   = &vk->backbuffers[swapchain_index];
   struct vk_descriptor_manager *manager         = &chain->descriptor_manager;
   struct vk_buffer_chain *buff_chain_vbo        = &chain->vbo;
   struct vk_buffer_chain *buff_chain_ubo        = &chain->ubo;

   vk->chain                                     = chain;
   vk->backbuffer                                = backbuffer;

   VK_DESCRIPTOR_MANAGER_RESTART(manager);
   VK_BUFFER_CHAIN_DISCARD(buff_chain_vbo);
   VK_BUFFER_CHAIN_DISCARD(buff_chain_ubo);

   /* Start recording the command buffer. */
   vk->cmd                                       = chain->cmd;

   begin_info.sType                              = 
      VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
   begin_info.pNext                              = NULL;
   begin_info.flags                              = 
      VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
   begin_info.pInheritanceInfo                   = NULL;

   vkResetCommandBuffer(vk->cmd, 0);

   vkBeginCommandBuffer(vk->cmd, &begin_info);

   vk->tracker.dirty                 = 0;
   vk->tracker.scissor.offset.x      = 0;
   vk->tracker.scissor.offset.y      = 0;
   vk->tracker.scissor.extent.width  = 0;
   vk->tracker.scissor.extent.height = 0;
   vk->flags                        &= ~VK_FLAG_TRACKER_USE_SCISSOR;
   vk->tracker.pipeline              = VK_NULL_HANDLE;
   vk->tracker.view                  = VK_NULL_HANDLE;
   vk->tracker.sampler               = VK_NULL_HANDLE;
   for (i = 0; i < 16; i++)
      vk->tracker.mvp.data[i]        = 0.0f;

   waits_for_semaphores              = 
       (vk->flags & VK_FLAG_HW_ENABLE) && frame &&
       !vk->hw.num_cmd && (vk->flags & VK_FLAG_HW_VALID_SEMAPHORE);

   if (waits_for_semaphores &&
       vk->hw.src_queue_family != VK_QUEUE_FAMILY_IGNORED &&
       vk->hw.src_queue_family != vk->context->graphics_queue_index)
   {
      retro_assert(vk->hw.image);

      /* Acquire ownership of image from other queue family. */
      VULKAN_TRANSFER_IMAGE_OWNERSHIP(vk->cmd,
            vk->hw.image->create_info.image,
            vk->hw.image->image_layout,
            /* Create a dependency chain from semaphore wait. */
            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
            VK_PIPELINE_STAGE_TRANSFER_BIT,
            vk->hw.src_queue_family, vk->context->graphics_queue_index);
   }

   /* Upload texture */
   if (frame && (!(vk->flags & VK_FLAG_HW_ENABLE)))
   {
      unsigned y;
      uint8_t *dst        = NULL;
      const uint8_t *src  = (const uint8_t*)frame;
      unsigned bpp        = vk->video.rgb32 ? 4 : 2;

      if (     chain->texture.width  != frame_width
            || chain->texture.height != frame_height)
      {
         chain->texture = vulkan_create_texture(vk, &chain->texture,
               frame_width, frame_height, chain->texture.format, NULL, NULL,
               chain->texture_optimal.memory
               ? VULKAN_TEXTURE_STAGING : VULKAN_TEXTURE_STREAMED);

         {
            struct vk_texture *texture = &chain->texture;
            VK_MAP_PERSISTENT_TEXTURE(vk->context->device, texture);
         }

         if (chain->texture.type == VULKAN_TEXTURE_STAGING)
            chain->texture_optimal = vulkan_create_texture(
                  vk,
                  &chain->texture_optimal,
                  frame_width, frame_height,
                  chain->texture_optimal.format,
                  NULL, NULL, VULKAN_TEXTURE_DYNAMIC);
      }

      if (frame != chain->texture.mapped)
      {
         dst = (uint8_t*)chain->texture.mapped;
         if (     (chain->texture.stride == pitch )
               && pitch == frame_width * bpp)
            memcpy(dst, src, frame_width * frame_height * bpp);
         else
            for (y = 0; y < frame_height; y++,
                  dst += chain->texture.stride, src += pitch)
               memcpy(dst, src, frame_width * bpp);
      }

      VULKAN_SYNC_TEXTURE_TO_GPU_COND_OBJ(vk, chain->texture);

      /* If we have an optimal texture, copy to that now. */
      if (chain->texture_optimal.memory != VK_NULL_HANDLE)
      {
         struct vk_texture *dynamic = &chain->texture_optimal;
         struct vk_texture *staging = &chain->texture;
         VULKAN_COPY_STAGING_TO_DYNAMIC(vk, vk->cmd, dynamic, staging);
      }

      vk->last_valid_index = frame_index;
   }

   /* Notify filter chain about the new sync index. */
   vulkan_filter_chain_notify_sync_index(
         (vulkan_filter_chain_t*)vk->filter_chain, frame_index);
   vulkan_filter_chain_set_frame_count(
         (vulkan_filter_chain_t*)vk->filter_chain, frame_count);
#ifdef HAVE_REWIND
   vulkan_filter_chain_set_frame_direction(
         (vulkan_filter_chain_t*)vk->filter_chain,
         state_manager_frame_is_reversed() ? -1 : 1);
#else
   vulkan_filter_chain_set_frame_direction(
         (vulkan_filter_chain_t*)vk->filter_chain,
         1);
#endif

   /* Render offscreen filter chain passes. */
   {
      /* Set the source texture in the filter chain */
      struct vulkan_filter_chain_texture input;

      if (vk->flags & VK_FLAG_HW_ENABLE)
      {
         /* Does this make that this can happen at all? */
         if (vk->hw.image && vk->hw.image->create_info.image)
         {
            if (frame)
            {
               input.width     = frame_width;
               input.height    = frame_height;
            }
            else
            {
               input.width     = vk->hw.last_width;
               input.height    = vk->hw.last_height;
            }

            input.image        = vk->hw.image->create_info.image;
            input.view         = vk->hw.image->image_view;
            input.layout       = vk->hw.image->image_layout;

            /* The format can change on a whim. */
            input.format       = vk->hw.image->create_info.format;
         }
         else
         {
            /* Fall back to the default, black texture.
             * This can happen if we restart the video 
             * driver while in the menu. */
            input.width        = vk->default_texture.width;
            input.height       = vk->default_texture.height;
            input.image        = vk->default_texture.image;
            input.view         = vk->default_texture.view;
            input.layout       = vk->default_texture.layout;
            input.format       = vk->default_texture.format;
         }

         vk->hw.last_width     = input.width;
         vk->hw.last_height    = input.height;
      }
      else
      {
         struct vk_texture *tex = &vk->swapchain[vk->last_valid_index].texture;
         if (vk->swapchain[vk->last_valid_index].texture_optimal.memory 
               != VK_NULL_HANDLE)
            tex = &vk->swapchain[vk->last_valid_index].texture_optimal;
         else if (tex->image)
            vulkan_transition_texture(vk, vk->cmd, tex);

         input.image  = tex->image;
         input.view   = tex->view;
         input.layout = tex->layout;
         input.width  = tex->width;
         input.height = tex->height;
         input.format = VK_FORMAT_UNDEFINED; /* It's already configured. */
      }

      vulkan_filter_chain_set_input_texture((vulkan_filter_chain_t*)
            vk->filter_chain, &input);
   }

   vulkan_set_viewport(vk, width, height, false, true);

   vulkan_filter_chain_build_offscreen_passes(
         (vulkan_filter_chain_t*)vk->filter_chain,
         vk->cmd, &vk->vk_vp);

#if defined(HAVE_MENU)
   /* Upload menu texture. */
   if (vk->flags & VK_FLAG_MENU_ENABLE)
   {
       if (vk->menu.textures[vk->menu.last_index].image != VK_NULL_HANDLE ||
           vk->menu.textures[vk->menu.last_index].buffer != VK_NULL_HANDLE)
       {
           struct vk_texture *optimal = &vk->menu.textures_optimal[vk->menu.last_index];
           struct vk_texture *texture = &vk->menu.textures[vk->menu.last_index];

           if (optimal->memory != VK_NULL_HANDLE)
           {
               if (vk->menu.dirty[vk->menu.last_index])
               {
                  struct vk_texture *dynamic = optimal;
                  struct vk_texture *staging = texture;
                  VULKAN_SYNC_TEXTURE_TO_GPU_COND_PTR(vk, staging);
                  VULKAN_COPY_STAGING_TO_DYNAMIC(vk, vk->cmd,
                        dynamic, staging);
                  vk->menu.dirty[vk->menu.last_index] = false;
               }
           }
       }
   }
#endif

#ifdef VULKAN_HDR_SWAPCHAIN
   if (use_main_buffer)
      backbuffer = &vk->main_buffer;
#endif /* VULKAN_HDR_SWAPCHAIN */

   /* Render to backbuffer. */
   if (     (backbuffer->image != VK_NULL_HANDLE)
         && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN))
   {
      rp_info.sType                    = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
      rp_info.pNext                    = NULL;
      rp_info.renderPass               = vk->render_pass;
      rp_info.framebuffer              = backbuffer->framebuffer;
      rp_info.renderArea.offset.x      = 0;
      rp_info.renderArea.offset.y      = 0;
      rp_info.renderArea.extent.width  = vk->context->swapchain_width;
      rp_info.renderArea.extent.height = vk->context->swapchain_height;
      rp_info.clearValueCount          = 1;
      rp_info.pClearValues             = &clear_color;

      clear_color.color.float32[0]     = 0.0f;
      clear_color.color.float32[1]     = 0.0f;
      clear_color.color.float32[2]     = 0.0f;
      clear_color.color.float32[3]     = 0.0f;

      /* Prepare backbuffer for rendering. */
      VULKAN_IMAGE_LAYOUT_TRANSITION(vk->cmd, backbuffer->image,
            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT,
            VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
            VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);

      /* Begin render pass and set up viewport */
      vkCmdBeginRenderPass(vk->cmd, &rp_info, VK_SUBPASS_CONTENTS_INLINE);

      vulkan_filter_chain_build_viewport_pass(
            (vulkan_filter_chain_t*)vk->filter_chain, vk->cmd,
            &vk->vk_vp, vk->mvp.data);

#ifdef HAVE_OVERLAY
      if ((vk->flags & VK_FLAG_OVERLAY_ENABLE) && overlay_behind_menu)
         vulkan_render_overlay(vk, video_width, video_height);
#endif

#if defined(HAVE_MENU)
      if (vk->flags & VK_FLAG_MENU_ENABLE)
      {
         menu_driver_frame(menu_is_alive, video_info);

         if (vk->menu.textures[vk->menu.last_index].image  != VK_NULL_HANDLE ||
             vk->menu.textures[vk->menu.last_index].buffer != VK_NULL_HANDLE)
         {
            struct vk_draw_quad quad;
            struct vk_texture *optimal = &vk->menu.textures_optimal[vk->menu.last_index];
            settings_t *settings       = config_get_ptr();
            bool menu_linear_filter    = settings->bools.menu_linear_filter;

            vulkan_set_viewport(vk, width, height, ((vk->flags &
                     VK_FLAG_MENU_FULLSCREEN) > 0), false);

            quad.pipeline              = vk->pipelines.alpha_blend;
            quad.texture               = &vk->menu.textures[vk->menu.last_index];

            if (optimal->memory != VK_NULL_HANDLE)
               quad.texture = optimal;

            if (menu_linear_filter)
               quad.sampler = (optimal->flags & VK_TEX_FLAG_MIPMAP) ?
                  vk->samplers.mipmap_linear : vk->samplers.linear;
            else
               quad.sampler = (optimal->flags & VK_TEX_FLAG_MIPMAP) ?
                  vk->samplers.mipmap_nearest : vk->samplers.nearest;

            quad.mvp        = &vk->mvp_no_rot;
            quad.color.r    = 1.0f;
            quad.color.g    = 1.0f;
            quad.color.b    = 1.0f;
            quad.color.a    = vk->menu.alpha;
            vulkan_draw_quad(vk, &quad);
         }
      }
      else if (statistics_show)
      {
         if (osd_params)
            font_driver_render_msg(vk,
                  stat_text,
                  osd_params, NULL);
      }
#endif

#ifdef HAVE_OVERLAY
      if ((vk->flags & VK_FLAG_OVERLAY_ENABLE) && !overlay_behind_menu)
         vulkan_render_overlay(vk, video_width, video_height);
#endif

      if (!string_is_empty(msg))
         font_driver_render_msg(vk, msg, NULL, NULL);

#ifdef HAVE_GFX_WIDGETS
      if (widgets_active)
         gfx_widgets_frame(video_info);
#endif

      /* End the render pass. We're done rendering to backbuffer now. */
      vkCmdEndRenderPass(vk->cmd);

#ifdef VULKAN_HDR_SWAPCHAIN
      /* Copy over back buffer to swap chain render targets */
      if (use_main_buffer)
      {
         backbuffer = &vk->backbuffers[swapchain_index];

         vulkan_hdr_uniform_t* mapped_ubo = (vulkan_hdr_uniform_t*)vk->hdr.ubo.mapped;

         mapped_ubo->mvp                  = vk->mvp_no_rot;          

         rp_info.sType                    = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
         rp_info.pNext                    = NULL;
         rp_info.renderPass               = vk->render_pass;
         rp_info.framebuffer              = backbuffer->framebuffer;
         rp_info.renderArea.offset.x      = 0;
         rp_info.renderArea.offset.y      = 0;
         rp_info.renderArea.extent.width  = vk->context->swapchain_width;
         rp_info.renderArea.extent.height = vk->context->swapchain_height;
         rp_info.clearValueCount          = 1;
         rp_info.pClearValues             = &clear_color;

         clear_color.color.float32[0]     = 0.0f;
         clear_color.color.float32[1]     = 0.0f;
         clear_color.color.float32[2]     = 0.0f;
         clear_color.color.float32[3]     = 0.0f;

         /* Prepare backbuffer for rendering. */
         VULKAN_IMAGE_LAYOUT_TRANSITION(vk->cmd, backbuffer->image,
               VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
               0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT,
               VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
               VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);

         VULKAN_IMAGE_LAYOUT_TRANSITION(vk->cmd, vk->main_buffer.image,
               VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
               VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
               VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
               VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);   

         /* Begin render pass and set up viewport */
         vkCmdBeginRenderPass(vk->cmd, &rp_info, VK_SUBPASS_CONTENTS_INLINE);

         {
            if (vk->pipelines.hdr != vk->tracker.pipeline)
            {
               vkCmdBindPipeline(vk->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vk->pipelines.hdr);
      
               vk->tracker.pipeline = vk->pipelines.hdr;
               /* Changing pipeline invalidates dynamic state. */
               vk->tracker.dirty   |= VULKAN_DIRTY_DYNAMIC_BIT;
            }
         }

         {
            VkWriteDescriptorSet write;
            VkDescriptorImageInfo image_info;
            VkDescriptorSet set = vulkan_descriptor_manager_alloc(
                  vk->context->device,
                  &vk->chain->descriptor_manager);     

            vulkan_set_uniform_buffer(vk->context->device,
                  set,
                  0,
                  vk->hdr.ubo.buffer,
                  0,
                  vk->hdr.ubo.size);                  

            image_info.sampler              = vk->samplers.nearest;
            image_info.imageView            = vk->main_buffer.view;
            image_info.imageLayout          = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

            write.sType                     = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
            write.pNext                     = NULL;
            write.dstSet                    = set;
            write.dstBinding                = 2;
            write.dstArrayElement           = 0;
            write.descriptorCount           = 1;
            write.descriptorType            = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
            write.pImageInfo                = &image_info;
            write.pBufferInfo               = NULL;
            write.pTexelBufferView          = NULL;
            
            vkUpdateDescriptorSets(vk->context->device, 1, &write, 0, NULL);
   
            vkCmdBindDescriptorSets(vk->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
                  vk->pipelines.layout, 0,
                  1, &set, 0, NULL);

            vk->tracker.view    = vk->main_buffer.view;
            vk->tracker.sampler = vk->samplers.nearest;
         }

         {
            VkViewport viewport;
            VkRect2D sci;

            viewport.x             = 0.0f;
            viewport.y             = 0.0f;
            viewport.width         = vk->context->swapchain_width;
            viewport.height        = vk->context->swapchain_height;
            viewport.minDepth      = 0.0f;
            viewport.maxDepth      = 1.0f;

            sci.offset.x           = (int32_t)viewport.x;
            sci.offset.y           = (int32_t)viewport.y;
            sci.extent.width       = (uint32_t)viewport.width;
            sci.extent.height      = (uint32_t)viewport.height;
            vkCmdSetViewport(vk->cmd, 0, 1, &viewport);
            vkCmdSetScissor(vk->cmd,  0, 1, &sci);
         }

         /* Upload VBO */
         {
            struct vk_buffer_range range;

            vulkan_buffer_chain_alloc(vk->context, &vk->chain->vbo, 6 * sizeof(struct vk_vertex), &range);

            {
               struct vk_vertex  *pv = (struct vk_vertex*)range.data;
               struct vk_color   color;

               color.r = 1.0f;
               color.g = 1.0f;
               color.b = 1.0f;
               color.a = 1.0f;               

               VULKAN_WRITE_QUAD_VBO(pv, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, &color);
            }

            vkCmdBindVertexBuffers(vk->cmd, 0, 1,
                  &range.buffer, &range.offset);
         }         

         vkCmdDraw(vk->cmd, 6, 1, 0, 0);

         vkCmdEndRenderPass(vk->cmd);
      }
#endif /* VULKAN_HDR_SWAPCHAIN */
   }

   /* End the filter chain frame.
    * This must happen outside a render pass.
    */
   vulkan_filter_chain_end_frame((vulkan_filter_chain_t*)vk->filter_chain, vk->cmd);

   if ( 
            (backbuffer->image != VK_NULL_HANDLE)
         && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
      )
   {
      if (     (vk->flags & VK_FLAG_READBACK_PENDING) 
            || (vk->flags & VK_FLAG_READBACK_STREAMED))
      {
         /* We cannot safely read back from an image which
          * has already been presented as we need to
          * maintain the PRESENT_SRC_KHR layout.
          *
          * If we're reading back, 
          * perform the readback before presenting.
          */
         VULKAN_IMAGE_LAYOUT_TRANSITION(
               vk->cmd,
               backbuffer->image,
               VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
               VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
               VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
               VK_ACCESS_TRANSFER_READ_BIT,
               VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
               VK_PIPELINE_STAGE_TRANSFER_BIT);

         vulkan_readback(vk);

         /* Prepare for presentation after transfers are complete. */
         VULKAN_IMAGE_LAYOUT_TRANSITION(
               vk->cmd,
               backbuffer->image,
               VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
               VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
               0,
               VK_ACCESS_MEMORY_READ_BIT,
               VK_PIPELINE_STAGE_TRANSFER_BIT,
               VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);

         vk->flags &= ~VK_FLAG_READBACK_PENDING;
      }
      else
      {
         /* Prepare backbuffer for presentation. */
         VULKAN_IMAGE_LAYOUT_TRANSITION(
               vk->cmd,
               backbuffer->image,
               VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
               VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
               VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
               0,
               VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
               VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
      }
   }

   if (waits_for_semaphores &&
       vk->hw.src_queue_family != VK_QUEUE_FAMILY_IGNORED &&
       vk->hw.src_queue_family != vk->context->graphics_queue_index)
   {
      retro_assert(vk->hw.image);

      /* Release ownership of image back to other queue family. */
      VULKAN_TRANSFER_IMAGE_OWNERSHIP(vk->cmd,
            vk->hw.image->create_info.image,
            vk->hw.image->image_layout,
            VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
            VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
            vk->context->graphics_queue_index, vk->hw.src_queue_family);
   }

   vkEndCommandBuffer(vk->cmd);

   /* Submit command buffers to GPU. */
   submit_info.sType                 = VK_STRUCTURE_TYPE_SUBMIT_INFO;
   submit_info.pNext                 = NULL;

   if (vk->hw.num_cmd)
   {
      /* vk->hw.cmd has already been allocated for this. */
      vk->hw.cmd[vk->hw.num_cmd]     = vk->cmd;

      submit_info.commandBufferCount = vk->hw.num_cmd + 1;
      submit_info.pCommandBuffers    = vk->hw.cmd;

      vk->hw.num_cmd                 = 0;
   }
   else
   {
      submit_info.commandBufferCount = 1;
      submit_info.pCommandBuffers    = &vk->cmd;
   }

   if (waits_for_semaphores)
   {
      submit_info.waitSemaphoreCount = vk->hw.num_semaphores;
      submit_info.pWaitSemaphores    = vk->hw.semaphores;
      submit_info.pWaitDstStageMask  = vk->hw.wait_dst_stages;

      /* Consume the semaphores. */
      vk->flags                     &= ~VK_FLAG_HW_VALID_SEMAPHORE;

      /* We allocated space for this. */
      if (    (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
           && (vk->context->swapchain_acquire_semaphore != VK_NULL_HANDLE))
      {
         assert(!vk->context->swapchain_wait_semaphores[frame_index]);
         vk->context->swapchain_wait_semaphores[frame_index] =
            vk->context->swapchain_acquire_semaphore;
         vk->context->swapchain_acquire_semaphore = VK_NULL_HANDLE;

         vk->hw.semaphores[submit_info.waitSemaphoreCount] = vk->context->swapchain_wait_semaphores[frame_index];
         vk->hw.wait_dst_stages[submit_info.waitSemaphoreCount] = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
         submit_info.waitSemaphoreCount++;
      }
   }
   else if ((vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
         && (vk->context->swapchain_acquire_semaphore != VK_NULL_HANDLE))
   {
      static const VkPipelineStageFlags wait_stage =
         VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;

      assert(!vk->context->swapchain_wait_semaphores[frame_index]);
      vk->context->swapchain_wait_semaphores[frame_index] =
         vk->context->swapchain_acquire_semaphore;
      vk->context->swapchain_acquire_semaphore = VK_NULL_HANDLE;

      submit_info.waitSemaphoreCount = 1;
      submit_info.pWaitSemaphores    = &vk->context->swapchain_wait_semaphores[frame_index];
      submit_info.pWaitDstStageMask  = &wait_stage;
   }
   else
   {
      submit_info.waitSemaphoreCount = 0;
      submit_info.pWaitSemaphores    = NULL;
      submit_info.pWaitDstStageMask  = NULL;
   }

   submit_info.signalSemaphoreCount  = 0;

   if ((vk->context->swapchain_semaphores[swapchain_index] 
         != VK_NULL_HANDLE)
         && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN))
      signal_semaphores[submit_info.signalSemaphoreCount++] = vk->context->swapchain_semaphores[swapchain_index];

   if (vk->hw.signal_semaphore != VK_NULL_HANDLE)
   {
      signal_semaphores[submit_info.signalSemaphoreCount++] = vk->hw.signal_semaphore;
      vk->hw.signal_semaphore = VK_NULL_HANDLE;
   }
   submit_info.pSignalSemaphores = submit_info.signalSemaphoreCount ? signal_semaphores : NULL;

#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
   vkQueueSubmit(vk->context->queue, 1,
         &submit_info, vk->context->swapchain_fences[frame_index]);
   vk->context->swapchain_fences_signalled[frame_index] = true;
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif

   if (vk->ctx_driver->swap_buffers)
      vk->ctx_driver->swap_buffers(vk->ctx_data);

   if (!(vk->context->flags & VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK))
   {
      if (vk->ctx_driver->update_window_title)
         vk->ctx_driver->update_window_title(vk->ctx_data);
   }

   /* Handle spurious swapchain invalidations as soon as we can,
    * i.e. right after swap buffers. */
#ifdef VULKAN_HDR_SWAPCHAIN
   bool video_hdr_enable          = video_info->hdr_enable;
   if (       (vk->flags & VK_FLAG_SHOULD_RESIZE)
         || (((vk->context->flags & VK_CTX_FLAG_HDR_ENABLE) > 0) 
         != video_hdr_enable))
#else
   if (vk->flags & VK_FLAG_SHOULD_RESIZE)
#endif /* VULKAN_HDR_SWAPCHAIN */
   {
#ifdef VULKAN_HDR_SWAPCHAIN
      if (video_hdr_enable)
      {
         vk->context->flags |= VK_CTX_FLAG_HDR_ENABLE;
#ifdef HAVE_THREADS
         slock_lock(vk->context->queue_lock);
#endif
         vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
         slock_unlock(vk->context->queue_lock);
#endif
         vulkan_destroy_hdr_buffer(vk->context->device, &vk->main_buffer);
      }
      else
         vk->context->flags &= ~VK_CTX_FLAG_HDR_ENABLE;

#endif /* VULKAN_HDR_SWAPCHAIN */

      gfx_ctx_mode_t mode;
      mode.width  = width;
      mode.height = height;

      if (vk->ctx_driver->set_resize)
         vk->ctx_driver->set_resize(vk->ctx_data, mode.width, mode.height);

#ifdef VULKAN_HDR_SWAPCHAIN
      if (   (vk->context->swapchain_colour_space)
          == VK_COLOR_SPACE_HDR10_ST2084_EXT)
         vk->flags          |=  VK_FLAG_HDR_SUPPORT;
      else
      {
         vk->flags          &= ~VK_FLAG_HDR_SUPPORT;
         vk->context->flags &= ~VK_CTX_FLAG_HDR_ENABLE;
      }

      if (vk->context->flags & VK_CTX_FLAG_HDR_ENABLE)
      {
         VkMemoryRequirements mem_reqs;
         VkImageCreateInfo image_info;
         VkMemoryAllocateInfo alloc;
         VkImageViewCreateInfo view;
         VkFramebufferCreateInfo info;

         memset(&vk->main_buffer, 0, sizeof(vk->main_buffer));

         /* Create the image */
         image_info.sType                = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
         image_info.pNext                = NULL;
         image_info.flags                = 0;
         image_info.imageType            = VK_IMAGE_TYPE_2D;
         image_info.format               = vk->context->swapchain_format;
         image_info.extent.width         = video_width;
         image_info.extent.height        = video_height;
         image_info.extent.depth         = 1;
         image_info.mipLevels            = 1;
         image_info.arrayLayers          = 1;
         image_info.samples              = VK_SAMPLE_COUNT_1_BIT;
         image_info.tiling               = VK_IMAGE_TILING_OPTIMAL;
         image_info.usage                = VK_IMAGE_USAGE_SAMPLED_BIT |
            VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
            VK_IMAGE_USAGE_TRANSFER_DST_BIT |
            VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
         image_info.sharingMode          = VK_SHARING_MODE_EXCLUSIVE;
         image_info.queueFamilyIndexCount= 0;
         image_info.pQueueFamilyIndices  = NULL;
         image_info.initialLayout        = VK_IMAGE_LAYOUT_UNDEFINED;

         vkCreateImage(vk->context->device, &image_info, NULL, &vk->main_buffer.image);
         vulkan_debug_mark_image(vk->context->device, vk->main_buffer.image);
         vkGetImageMemoryRequirements(vk->context->device, vk->main_buffer.image, &mem_reqs);
         alloc.sType                     = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
         alloc.pNext                     = NULL;
         alloc.allocationSize            = mem_reqs.size;
         alloc.memoryTypeIndex           = vulkan_find_memory_type(
               &vk->context->memory_properties,
               mem_reqs.memoryTypeBits,
               VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

         vkAllocateMemory(vk->context->device, &alloc, NULL, &vk->main_buffer.memory);
         vulkan_debug_mark_memory(vk->context->device, vk->main_buffer.memory);

         vkBindImageMemory(vk->context->device, vk->main_buffer.image, vk->main_buffer.memory, 0);

         /* Create an image view which we can render into. */
         view.sType                           = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
         view.pNext                           = NULL;
         view.flags                           = 0;
         view.image                           = vk->main_buffer.image;
         view.viewType                        = VK_IMAGE_VIEW_TYPE_2D;
         view.format                          = image_info.format;
         view.components.r                    = VK_COMPONENT_SWIZZLE_R;
         view.components.g                    = VK_COMPONENT_SWIZZLE_G;
         view.components.b                    = VK_COMPONENT_SWIZZLE_B;
         view.components.a                    = VK_COMPONENT_SWIZZLE_A;
         view.subresourceRange.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
         view.subresourceRange.baseMipLevel   = 0;
         view.subresourceRange.levelCount     = 1;
         view.subresourceRange.baseArrayLayer = 0;
         view.subresourceRange.layerCount     = 1;

         vkCreateImageView(vk->context->device, &view, NULL, &vk->main_buffer.view);

         /* Create the framebuffer */
         info.sType           = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
         info.pNext           = NULL;
         info.flags           = 0;
         info.renderPass      = vk->render_pass;
         info.attachmentCount = 1;
         info.pAttachments    = &vk->main_buffer.view;
         info.width           = vk->context->swapchain_width;
         info.height          = vk->context->swapchain_height;
         info.layers          = 1;

         vkCreateFramebuffer(vk->context->device, &info, NULL, &vk->main_buffer.framebuffer);
      }
#endif /* VULKAN_HDR_SWAPCHAIN */
      vk->flags &= ~VK_FLAG_SHOULD_RESIZE;
   }

   if (vk->context->flags & VK_CTX_FLAG_INVALID_SWAPCHAIN)
      vulkan_check_swapchain(vk); 

   /* Disable BFI during fast forward, slow-motion,
    * and pause to prevent flicker. */
   if (
            (backbuffer->image != VK_NULL_HANDLE)
         && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)
         && black_frame_insertion
         && !input_driver_nonblock_state
         && !runloop_is_slowmotion
         && !runloop_is_paused
         && (!(vk->flags & VK_FLAG_MENU_ENABLE)))
   {   
      int n;
      for (n = 0; n < black_frame_insertion; ++n)
      {
         vulkan_inject_black_frame(vk, video_info);
         if (vk->ctx_driver->swap_buffers)
            vk->ctx_driver->swap_buffers(vk->ctx_data);
      }
   }

   /* Vulkan doesn't directly support swap_interval > 1, 
    * so we fake it by duping out more frames. */
   if (      (vk->context->swap_interval > 1)
         && (!(vk->context->flags & VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK)))
   {
      int i;
      vk->context->flags |= VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK;
      for (i = 1; i < vk->context->swap_interval; i++)
      {
         if (!vulkan_frame(vk, NULL, 0, 0, frame_count, 0, msg,
                  video_info))
         {
            vk->context->flags &= ~VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK;
            return false;
         }
      }
      vk->context->flags &= ~VK_CTX_FLAG_SWAP_INTERVAL_EMULATION_LOCK;
   }

   return true;
}

static void vulkan_set_aspect_ratio(void *data, unsigned aspect_ratio_idx)
{
   vk_t *vk = (vk_t*)data;
   if (vk)
      vk->flags |= VK_FLAG_KEEP_ASPECT | VK_FLAG_SHOULD_RESIZE;
}

static void vulkan_apply_state_changes(void *data)
{
   vk_t *vk = (vk_t*)data;
   if (vk)
      vk->flags |= VK_FLAG_SHOULD_RESIZE;
}

static void vulkan_show_mouse(void *data, bool state)
{
   vk_t                            *vk = (vk_t*)data;

   if (vk && vk->ctx_driver->show_mouse)
      vk->ctx_driver->show_mouse(vk->ctx_data, state);
}

static struct video_shader *vulkan_get_current_shader(void *data)
{
   vk_t *vk = (vk_t*)data;
   if (vk && vk->filter_chain)
      return vulkan_filter_chain_get_preset((vulkan_filter_chain_t*)vk->filter_chain);
   return NULL;
}

static bool vulkan_get_current_sw_framebuffer(void *data,
      struct retro_framebuffer *framebuffer)
{
   struct vk_per_frame *chain = NULL;
   vk_t *vk                   = (vk_t*)data;
   vk->chain                  =
      &vk->swapchain[vk->context->current_frame_index];
   chain                      = vk->chain;

   if (chain->texture.width != framebuffer->width ||
         chain->texture.height != framebuffer->height)
   {
      chain->texture   = vulkan_create_texture(vk, &chain->texture,
            framebuffer->width, framebuffer->height, chain->texture.format,
            NULL, NULL, VULKAN_TEXTURE_STREAMED);
      {
         struct vk_texture *texture = &chain->texture;
         VK_MAP_PERSISTENT_TEXTURE(vk->context->device, texture);
      }

      if (chain->texture.type == VULKAN_TEXTURE_STAGING)
      {
         chain->texture_optimal = vulkan_create_texture(
               vk,
               &chain->texture_optimal,
               framebuffer->width,
               framebuffer->height,
               chain->texture_optimal.format,
               NULL, NULL, VULKAN_TEXTURE_DYNAMIC);
      }
   }

   framebuffer->data         = chain->texture.mapped;
   framebuffer->pitch        = chain->texture.stride;
   framebuffer->format       = vk->video.rgb32
      ? RETRO_PIXEL_FORMAT_XRGB8888 : RETRO_PIXEL_FORMAT_RGB565;
   framebuffer->memory_flags = 0;

   if (vk->context->memory_properties.memoryTypes[
         chain->texture.memory_type].propertyFlags &
         VK_MEMORY_PROPERTY_HOST_CACHED_BIT)
      framebuffer->memory_flags |= RETRO_MEMORY_TYPE_CACHED;

   return true;
}

static bool vulkan_is_mapped_swapchain_texture_ptr(const vk_t* vk,
      const void* ptr)
{
   int i;
   for (i = 0; i < vk->num_swapchain_images; i++)
   {
      if (ptr == vk->swapchain[i].texture.mapped)
         return true;
   }

   return false;
}

static bool vulkan_get_hw_render_interface(void *data,
      const struct retro_hw_render_interface **iface)
{
   vk_t *vk = (vk_t*)data;
   *iface   = (const struct retro_hw_render_interface*)&vk->hw.iface;
   return ((vk->flags & VK_FLAG_HW_ENABLE) > 0);
}

static void vulkan_set_texture_frame(void *data,
      const void *frame, bool rgb32, unsigned width, unsigned height,
      float alpha)
{
   unsigned y, stride;
   uint8_t *ptr                        = NULL;
   uint8_t *dst                        = NULL;
   const uint8_t *src                  = NULL;
   vk_t *vk                            = (vk_t*)data;
   unsigned idx                        = 0;
   struct vk_texture *texture          = NULL;
   struct vk_texture *texture_optimal  = NULL;
   const VkComponentMapping br_swizzle = {
      VK_COMPONENT_SWIZZLE_B,
      VK_COMPONENT_SWIZZLE_G,
      VK_COMPONENT_SWIZZLE_R,
      VK_COMPONENT_SWIZZLE_A,
   };

   if (!vk)
      return;

   idx             = vk->context->current_frame_index;
   texture         = &vk->menu.textures[idx  ];
   texture_optimal = &vk->menu.textures_optimal[idx  ];

   /* B4G4R4A4 must be supported, but R4G4B4A4 is optional,
    * just apply the swizzle in the image view instead. */
   *texture = vulkan_create_texture(vk,
         texture->memory ? texture : NULL,
         width, height,
         rgb32 ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_B4G4R4A4_UNORM_PACK16,
         NULL, rgb32 ? NULL : &br_swizzle,
         texture_optimal->memory ? VULKAN_TEXTURE_STAGING : VULKAN_TEXTURE_STREAMED);

   vkMapMemory(vk->context->device, texture->memory,
         texture->offset, texture->size, 0, (void**)&ptr);

   dst       = ptr;
   src       = (const uint8_t*)frame;
   stride    = (rgb32 ? sizeof(uint32_t) : sizeof(uint16_t)) * width;

   for (y = 0; y < height; y++, dst += texture->stride, src += stride)
      memcpy(dst, src, stride);

   vk->menu.alpha      = alpha;
   vk->menu.last_index = idx;

   if (texture->type == VULKAN_TEXTURE_STAGING)
      *texture_optimal = vulkan_create_texture(vk,
            texture_optimal->memory ? texture_optimal : NULL,
            width, height,
            rgb32 ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_B4G4R4A4_UNORM_PACK16,
            NULL, rgb32 ? NULL : &br_swizzle,
            VULKAN_TEXTURE_DYNAMIC);
   else
   {
      VULKAN_SYNC_TEXTURE_TO_GPU_COND_PTR(vk, texture);
   }

   vkUnmapMemory(vk->context->device, texture->memory);
   vk->menu.dirty[idx] = true;
}

static void vulkan_set_texture_enable(void *data, bool state, bool fullscreen)
{
   vk_t *vk = (vk_t*)data;
   if (!vk)
      return;

   if (state)
      vk->flags        |=  VK_FLAG_MENU_ENABLE;
   else
      vk->flags        &= ~VK_FLAG_MENU_ENABLE;
   if (fullscreen)
      vk->flags        |=  VK_FLAG_MENU_FULLSCREEN;
   else
      vk->flags        &= ~VK_FLAG_MENU_FULLSCREEN;
}

#define VK_T0 0xff000000u
#define VK_T1 0xffffffffu

static uintptr_t vulkan_load_texture(void *video_data, void *data,
      bool threaded, enum texture_filter_type filter_type)
{
   struct vk_texture *texture  = NULL;
   vk_t *vk                    = (vk_t*)video_data;
   struct texture_image *image = (struct texture_image*)data;
   if (!image)
      return 0;

   if (!(texture = (struct vk_texture*)calloc(1, sizeof(*texture))))
      return 0;

   if (!image->pixels || !image->width || !image->height)
   {
      /* Create a dummy texture instead. */
      static const uint32_t checkerboard[] = {
         VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,
         VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,
         VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,
         VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,
         VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,
         VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,
         VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,
         VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,
      };
      *texture                = vulkan_create_texture(vk, NULL,
            8, 8, VK_FORMAT_B8G8R8A8_UNORM,
            checkerboard, NULL, VULKAN_TEXTURE_STATIC);
      texture->flags         &= ~(VK_TEX_FLAG_DEFAULT_SMOOTH
                                | VK_TEX_FLAG_MIPMAP);
   }
   else
   {
      *texture = vulkan_create_texture(vk, NULL,
            image->width, image->height, VK_FORMAT_B8G8R8A8_UNORM,
            image->pixels, NULL, VULKAN_TEXTURE_STATIC);
      if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR || filter_type ==
            TEXTURE_FILTER_LINEAR)
         texture->flags |= VK_TEX_FLAG_DEFAULT_SMOOTH;
      if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR)
         texture->flags |= VK_TEX_FLAG_MIPMAP;
   }

   return (uintptr_t)texture;
}

static void vulkan_unload_texture(void *data, 
      bool threaded, uintptr_t handle)
{
   vk_t *vk                         = (vk_t*)data;
   struct vk_texture *texture       = (struct vk_texture*)handle;
   if (!texture || !vk)
      return;

   /* TODO: We really want to defer this deletion instead,
    * but this will do for now. */
#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
   vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif
   vulkan_destroy_texture(
         vk->context->device, texture);
   free(texture);
}

static float vulkan_get_refresh_rate(void *data)
{
   float refresh_rate;

   if (video_context_driver_get_refresh_rate(&refresh_rate))
       return refresh_rate;

   return 0.0f;
}

static uint32_t vulkan_get_flags(void *data)
{
   uint32_t flags = 0;

   BIT32_SET(flags, GFX_CTX_FLAGS_CUSTOMIZABLE_SWAPCHAIN_IMAGES);
   BIT32_SET(flags, GFX_CTX_FLAGS_BLACK_FRAME_INSERTION);
   BIT32_SET(flags, GFX_CTX_FLAGS_MENU_FRAME_FILTERING);
   BIT32_SET(flags, GFX_CTX_FLAGS_SCREENSHOTS_SUPPORTED);
   BIT32_SET(flags, GFX_CTX_FLAGS_OVERLAY_BEHIND_MENU_SUPPORTED);

   return flags;
}

static void vulkan_get_video_output_size(void *data,
      unsigned *width, unsigned *height, char *desc, size_t desc_len)
{
   vk_t *vk = (vk_t*)data;
   if (vk && vk->ctx_driver && vk->ctx_driver->get_video_output_size)
      vk->ctx_driver->get_video_output_size(
            vk->ctx_data,
            width, height, desc, desc_len);
}

static void vulkan_get_video_output_prev(void *data)
{
   vk_t *vk = (vk_t*)data;
   if (vk && vk->ctx_driver && vk->ctx_driver->get_video_output_prev)
      vk->ctx_driver->get_video_output_prev(vk->ctx_data);
}

static void vulkan_get_video_output_next(void *data)
{
   vk_t *vk = (vk_t*)data;
   if (vk && vk->ctx_driver && vk->ctx_driver->get_video_output_next)
      vk->ctx_driver->get_video_output_next(vk->ctx_data);
}

static const video_poke_interface_t vulkan_poke_interface = {
   vulkan_get_flags,
   vulkan_load_texture,
   vulkan_unload_texture,
   vulkan_set_video_mode,
   vulkan_get_refresh_rate, /* get_refresh_rate */
   NULL,
   vulkan_get_video_output_size,
   vulkan_get_video_output_prev,
   vulkan_get_video_output_next,
   NULL,
   NULL,
   vulkan_set_aspect_ratio,
   vulkan_apply_state_changes,
   vulkan_set_texture_frame,
   vulkan_set_texture_enable,
   font_driver_render_msg,
   vulkan_show_mouse,
   NULL,                               /* grab_mouse_toggle */
   vulkan_get_current_shader,
   vulkan_get_current_sw_framebuffer,
   vulkan_get_hw_render_interface,
#ifdef VULKAN_HDR_SWAPCHAIN   
   vulkan_set_hdr_max_nits,
   vulkan_set_hdr_paper_white_nits,
   vulkan_set_hdr_contrast,
   vulkan_set_hdr_expand_gamut
#else
   NULL, /* set_hdr_max_nits */
   NULL, /* set_hdr_paper_white_nits */
   NULL, /* set_hdr_contrast */
   NULL  /* set_hdr_expand_gamut */
#endif /* VULKAN_HDR_SWAPCHAIN */
};

static void vulkan_get_poke_interface(void *data,
      const video_poke_interface_t **iface)
{
   (void)data;
   *iface = &vulkan_poke_interface;
}

static void vulkan_viewport_info(void *data, struct video_viewport *vp)
{
   unsigned width, height;
   vk_t *vk = (vk_t*)data;

   if (!vk)
      return;

   width           = vk->video_width;
   height          = vk->video_height;
   /* Make sure we get the correct viewport. */
   vulkan_set_viewport(vk, width, height, false, true);

   *vp             = vk->vp;
   vp->full_width  = width;
   vp->full_height = height;
}

static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle)
{
   struct vk_texture *staging       = NULL;
   vk_t *vk                         = (vk_t*)data;

   if (!vk)
      return false;

   staging = &vk->readback.staging[vk->context->current_frame_index];

   if (vk->flags & VK_FLAG_READBACK_STREAMED)
   {
      const uint8_t *src     = NULL;
      struct scaler_ctx *ctx = NULL;

      switch (vk->context->swapchain_format)
      {
         case VK_FORMAT_R8G8B8A8_UNORM:
         case VK_FORMAT_A8B8G8R8_UNORM_PACK32:
            ctx = &vk->readback.scaler_rgb;
            break;

         case VK_FORMAT_B8G8R8A8_UNORM:
            ctx = &vk->readback.scaler_bgr;
            break;

         default:
            RARCH_ERR("[Vulkan]: Unexpected swapchain format. Cannot readback.\n");
            break;
      }

      if (ctx)
      {
         if (staging->memory == VK_NULL_HANDLE)
            return false;

         buffer += 3 * (vk->vp.height - 1) * vk->vp.width;
         vkMapMemory(vk->context->device, staging->memory,
               staging->offset, staging->size, 0, (void**)&src);

         if (     (staging->flags & VK_TEX_FLAG_NEED_MANUAL_CACHE_MANAGEMENT)
               && (staging->memory != VK_NULL_HANDLE))
            VULKAN_SYNC_TEXTURE_TO_CPU(vk->context->device, staging->memory);

         ctx->in_stride  = staging->stride;
         ctx->out_stride = -(int)vk->vp.width * 3;
         scaler_ctx_scale_direct(ctx, buffer, src);

         vkUnmapMemory(vk->context->device, staging->memory);
      }
   }
   else
   {
      /* Synchronous path only for now. */
      /* TODO: How will we deal with format conversion?
       * For now, take the simplest route and use image blitting
       * with conversion. */
      vk->flags |= VK_FLAG_READBACK_PENDING;

      if (!is_idle)
         video_driver_cached_frame();

#ifdef HAVE_THREADS
      slock_lock(vk->context->queue_lock);
#endif
      vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
      slock_unlock(vk->context->queue_lock);
#endif

      if (!staging->memory)
      {
         RARCH_ERR("[Vulkan]: Attempted to readback synchronously, but no image is present.\nThis can happen if vsync is disabled on Windows systems due to mailbox emulation.\n");
         return false;
      }

      if (!staging->mapped)
      {
         VK_MAP_PERSISTENT_TEXTURE(vk->context->device, staging);
      }

      if (     (staging->flags & VK_TEX_FLAG_NEED_MANUAL_CACHE_MANAGEMENT)
            && (staging->memory != VK_NULL_HANDLE))
         VULKAN_SYNC_TEXTURE_TO_CPU(vk->context->device, staging->memory);

      {
         int y;
         const uint8_t *src = (const uint8_t*)staging->mapped;
         buffer            += 3 * (vk->vp.height - 1) * vk->vp.width;

         switch (vk->context->swapchain_format)
         {
            case VK_FORMAT_B8G8R8A8_UNORM:
               for (y = 0; y < vk->vp.height; y++,
                     src += staging->stride, buffer -= 3 * vk->vp.width)
               {
                  int x;
                  for (x = 0; x < vk->vp.width; x++)
                  {
                     buffer[3 * x + 0] = src[4 * x + 0];
                     buffer[3 * x + 1] = src[4 * x + 1];
                     buffer[3 * x + 2] = src[4 * x + 2];
                  }
               }
               break;

            case VK_FORMAT_R8G8B8A8_UNORM:
            case VK_FORMAT_A8B8G8R8_UNORM_PACK32:
               for (y = 0; y < vk->vp.height; y++,
                     src += staging->stride, buffer -= 3 * vk->vp.width)
               {
                  int x;
                  for (x = 0; x < vk->vp.width; x++)
                  {
                     buffer[3 * x + 2] = src[4 * x + 0];
                     buffer[3 * x + 1] = src[4 * x + 1];
                     buffer[3 * x + 0] = src[4 * x + 2];
                  }
               }
               break;

            default:
               RARCH_ERR("[Vulkan]: Unexpected swapchain format.\n");
               break;
         }
      }
      vulkan_destroy_texture(
            vk->context->device, staging);
   }
   return true;
}

#ifdef HAVE_OVERLAY
static void vulkan_overlay_enable(void *data, bool enable)
{
   vk_t *vk = (vk_t*)data;
   if (!vk)
      return;

   if (enable)
      vk->flags |=  VK_FLAG_OVERLAY_ENABLE;
   else
      vk->flags &= ~VK_FLAG_OVERLAY_ENABLE;
   if (vk->ctx_driver->show_mouse)
      vk->ctx_driver->show_mouse(vk->ctx_data, enable);
}

static void vulkan_overlay_full_screen(void *data, bool enable)
{
   vk_t *vk = (vk_t*)data;
   if (!vk)
      return;

   if (enable)
      vk->flags |=  VK_FLAG_OVERLAY_FULLSCREEN;
   else
      vk->flags &= ~VK_FLAG_OVERLAY_FULLSCREEN;
}

static void vulkan_overlay_free(vk_t *vk)
{
   int i;
   if (!vk)
      return;

   free(vk->overlay.vertex);
   for (i = 0; i < vk->overlay.count; i++)
      if (vk->overlay.images[i].memory != VK_NULL_HANDLE)
         vulkan_destroy_texture(
               vk->context->device,
               &vk->overlay.images[i]);

   if (vk->overlay.images)
      free(vk->overlay.images);

   memset(&vk->overlay, 0, sizeof(vk->overlay));
}

static void vulkan_overlay_set_alpha(void *data,
      unsigned image, float mod)
{
   int i;
   struct vk_vertex *pv;
   vk_t *vk = (vk_t*)data;

   if (!vk)
      return;

   pv = &vk->overlay.vertex[image * 4];
   for (i = 0; i < 4; i++)
   {
      pv[i].color.r = 1.0f;
      pv[i].color.g = 1.0f;
      pv[i].color.b = 1.0f;
      pv[i].color.a = mod;
   }
}

static void vulkan_render_overlay(vk_t *vk, unsigned width,
      unsigned height)
{
   int i;
   struct video_viewport vp;

   if (!vk)
      return;

   vp                       = vk->vp;
   vulkan_set_viewport(vk, width, height,
         ((vk->flags & VK_FLAG_OVERLAY_FULLSCREEN) > 0),
         false);

   for (i = 0; i < vk->overlay.count; i++)
   {
      struct vk_draw_triangles call;
      struct vk_buffer_range range;

      if (!vulkan_buffer_chain_alloc(vk->context, &vk->chain->vbo,
               4 * sizeof(struct vk_vertex), &range))
         break;

      memcpy(range.data, &vk->overlay.vertex[i * 4],
            4 * sizeof(struct vk_vertex));

      call.vertices     = 4;
      call.uniform_size = sizeof(vk->mvp);
      call.uniform      = &vk->mvp;
      call.vbo          = &range;
      call.texture      = &vk->overlay.images[i];
      call.pipeline     = vk->display.pipelines[3]; /* Strip with blend */
      call.sampler      = (call.texture->flags & VK_TEX_FLAG_MIPMAP)
         ? vk->samplers.mipmap_linear : vk->samplers.linear;
      vulkan_draw_triangles(vk, &call);
   }

   /* Restore the viewport so we don't mess with recording. */
   vk->vp = vp;
}

static void vulkan_overlay_vertex_geom(void *data, unsigned image,
      float x, float y,
      float w, float h)
{
   struct vk_vertex *pv = NULL;
   vk_t             *vk = (vk_t*)data;
   if (!vk)
      return;

   pv      = &vk->overlay.vertex[4 * image];

   pv[0].x = x;
   pv[0].y = y;
   pv[1].x = x;
   pv[1].y = y + h;
   pv[2].x = x + w;
   pv[2].y = y;
   pv[3].x = x + w;
   pv[3].y = y + h;
}

static void vulkan_overlay_tex_geom(void *data, unsigned image,
      float x, float y,
      float w, float h)
{
   struct vk_vertex *pv = NULL;
   vk_t *vk             = (vk_t*)data;
   if (!vk)
      return;

   pv          = &vk->overlay.vertex[4 * image];

   pv[0].tex_x = x;
   pv[0].tex_y = y;
   pv[1].tex_x = x;
   pv[1].tex_y = y + h;
   pv[2].tex_x = x + w;
   pv[2].tex_y = y;
   pv[3].tex_x = x + w;
   pv[3].tex_y = y + h;
}

static bool vulkan_overlay_load(void *data,
      const void *image_data, unsigned num_images)
{
   int i;
   bool old_enabled;
   const struct texture_image *images =
      (const struct texture_image*)image_data;
   vk_t *vk                           = (vk_t*)data;
   static const struct vk_color white = {
      1.0f, 1.0f, 1.0f, 1.0f,
   };

   if (!vk)
      return false;

#ifdef HAVE_THREADS
   slock_lock(vk->context->queue_lock);
#endif
   vkQueueWaitIdle(vk->context->queue);
#ifdef HAVE_THREADS
   slock_unlock(vk->context->queue_lock);
#endif
   old_enabled        = vk->flags & VK_FLAG_OVERLAY_ENABLE;
   vulkan_overlay_free(vk);

   if (!(vk->overlay.images = (struct vk_texture*)
            calloc(num_images, sizeof(*vk->overlay.images))))
      goto error;
   vk->overlay.count  = num_images;

   if (!(vk->overlay.vertex = (struct vk_vertex*)
      calloc(4 * num_images, sizeof(*vk->overlay.vertex))))
      goto error;

   for (i = 0; i < num_images; i++)
   {
      int j;
      vk->overlay.images[i] = vulkan_create_texture(vk, NULL,
            images[i].width, images[i].height,
            VK_FORMAT_B8G8R8A8_UNORM, images[i].pixels,
            NULL, VULKAN_TEXTURE_STATIC);

      vulkan_overlay_tex_geom(vk, i, 0, 0, 1, 1);
      vulkan_overlay_vertex_geom(vk, i, 0, 0, 1, 1);
      for (j = 0; j < 4; j++)
         vk->overlay.vertex[4 * i + j].color = white;
   }

   if (old_enabled)
      vk->flags |=  VK_FLAG_OVERLAY_ENABLE;
   else
      vk->flags &= ~VK_FLAG_OVERLAY_ENABLE;

   return true;

error:
   vulkan_overlay_free(vk);
   return false;
}

static const video_overlay_interface_t vulkan_overlay_interface = {
   vulkan_overlay_enable,
   vulkan_overlay_load,
   vulkan_overlay_tex_geom,
   vulkan_overlay_vertex_geom,
   vulkan_overlay_full_screen,
   vulkan_overlay_set_alpha,
};

static void vulkan_get_overlay_interface(void *data,
      const video_overlay_interface_t **iface) { *iface = &vulkan_overlay_interface; }
#endif

#ifdef HAVE_GFX_WIDGETS
static bool vulkan_gfx_widgets_enabled(void *data) { return true; }
#endif

static bool vulkan_has_windowed(void *data)
{
   vk_t *vk        = (vk_t*)data;
   if (vk && vk->ctx_driver)
      return vk->ctx_driver->has_windowed;
   return false;
}

static bool vulkan_focus(void *data)
{
   vk_t *vk        = (vk_t*)data;
   if (vk && vk->ctx_driver && vk->ctx_driver->has_focus)
      return vk->ctx_driver->has_focus(vk->ctx_data);
   return true;
}

video_driver_t video_vulkan = {
   vulkan_init,
   vulkan_frame,
   vulkan_set_nonblock_state,
   vulkan_alive,
   vulkan_focus,
   vulkan_suppress_screensaver,
   vulkan_has_windowed,
   vulkan_set_shader,
   vulkan_free,
   "vulkan",
   vulkan_set_viewport,
   vulkan_set_rotation,
   vulkan_viewport_info,
   vulkan_read_viewport,
   NULL,                         /* vulkan_read_frame_raw */

#ifdef HAVE_OVERLAY
   vulkan_get_overlay_interface,
#endif
#ifdef HAVE_VIDEO_LAYOUT
   NULL,
#endif
   vulkan_get_poke_interface,
   NULL,                         /* vulkan_wrap_type_to_enum */
#ifdef HAVE_GFX_WIDGETS
   vulkan_gfx_widgets_enabled
#endif
};
