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

#define AMD_GPUPERFAPI_SKIP_VULKAN_INCLUDE 1

#include "vk_debug.h"
#include <float.h>
#include "core/settings.h"
#include "data/glsl_shaders.h"
#include "driver/ihv/amd/amd_counters.h"
#include "driver/ihv/amd/official/GPUPerfAPI/Include/gpu_perf_api_vk.h"
#include "driver/shaders/spirv/spirv_compile.h"
#include "maths/camera.h"
#include "maths/formatpacking.h"
#include "maths/matrix.h"
#include "vk_core.h"
#include "vk_replay.h"
#include "vk_shader_cache.h"

#define VULKAN 1
#include "data/glsl/glsl_ubos_cpp.h"

RDOC_CONFIG(bool, Vulkan_HardwareCounters, true,
            "Enable support for IHV-specific hardware counters on Vulkan.");

const VkDeviceSize STAGE_BUFFER_BYTE_SIZE = 16 * 1024 * 1024ULL;

static void create(WrappedVulkan *driver, const char *objName, const int line, VkSampler *sampler,
                   VkFilter samplerFilter)
{
  VkSamplerCreateInfo sampInfo = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};

  sampInfo.minFilter = sampInfo.magFilter = samplerFilter;
  sampInfo.mipmapMode = samplerFilter == VK_FILTER_NEAREST ? VK_SAMPLER_MIPMAP_MODE_NEAREST
                                                           : VK_SAMPLER_MIPMAP_MODE_LINEAR;
  sampInfo.addressModeU = sampInfo.addressModeV = sampInfo.addressModeW =
      VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
  sampInfo.maxLod = 128.0f;

  VkResult vkr = driver->vkCreateSampler(driver->GetDev(), &sampInfo, NULL, sampler);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

static void create(WrappedVulkan *driver, const char *objName, const int line,
                   VkDescriptorSetLayout *descLayout,
                   std::initializer_list<VkDescriptorSetLayoutBinding> bindings)
{
  VkDescriptorSetLayoutCreateInfo descsetLayoutInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
      NULL,
      0,
      (uint32_t)bindings.size(),
      bindings.begin(),
  };

  VkResult vkr =
      driver->vkCreateDescriptorSetLayout(driver->GetDev(), &descsetLayoutInfo, NULL, descLayout);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

static void create(WrappedVulkan *driver, const char *objName, const int line,
                   VkPipelineLayout *pipeLayout, VkDescriptorSetLayout setLayout, uint32_t pushBytes)
{
  VkPipelineLayoutCreateInfo pipeLayoutInfo = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};

  VkPushConstantRange push = {VK_SHADER_STAGE_ALL, 0, pushBytes};

  if(pushBytes > 0)
  {
    pipeLayoutInfo.pPushConstantRanges = &push;
    pipeLayoutInfo.pushConstantRangeCount = 1;
  }

  pipeLayoutInfo.pSetLayouts = &setLayout;
  pipeLayoutInfo.setLayoutCount = 1;

  VkResult vkr = driver->vkCreatePipelineLayout(driver->GetDev(), &pipeLayoutInfo, NULL, pipeLayout);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

// create a single subpass renderpass with a single attachment
static void create(WrappedVulkan *driver, const char *objName, const int line,
                   VkRenderPass *renderPass, VkFormat attachFormat,
                   VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT,
                   VkImageLayout layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
{
  VkAttachmentDescription attDesc = {0,
                                     attachFormat,
                                     sampleCount,
                                     VK_ATTACHMENT_LOAD_OP_LOAD,
                                     VK_ATTACHMENT_STORE_OP_STORE,
                                     VK_ATTACHMENT_LOAD_OP_DONT_CARE,
                                     VK_ATTACHMENT_STORE_OP_DONT_CARE,
                                     layout,
                                     layout};

  VkAttachmentReference attRef = {0, layout};

  VkSubpassDescription sub = {
      0, VK_PIPELINE_BIND_POINT_GRAPHICS,
      0, NULL,       // inputs
      1, &attRef,    // color
  };

  if(IsDepthOrStencilFormat(attachFormat))
  {
    attDesc.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    attDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    attDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    attDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;

    sub.colorAttachmentCount = 0;
    sub.pColorAttachments = NULL;
    sub.pDepthStencilAttachment = &attRef;
  }

  VkRenderPassCreateInfo rpinfo = {
      VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, NULL, 0, 1, &attDesc, 1, &sub,
  };

  VkResult vkr = driver->vkCreateRenderPass(driver->GetDev(), &rpinfo, NULL, renderPass);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());

  driver->GetResourceManager()->SetInternalResource(GetResID(*renderPass));
}

// Create a compute pipeline with a shader module
static void create(WrappedVulkan *driver, const char *objName, const int line, VkPipeline *pipe,
                   VkPipelineLayout pipeLayout, VkShaderModule computeModule)
{
  // if the module didn't compile, this pipeline is not be supported. Silently don't create it, code
  // later should handle the missing pipeline as indicating lack of support
  if(computeModule == VK_NULL_HANDLE)
  {
    *pipe = VK_NULL_HANDLE;
    return;
  }

  VkComputePipelineCreateInfo compPipeInfo = {
      VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
      NULL,
      0,
      {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_COMPUTE_BIT,
       computeModule, "main", NULL},
      pipeLayout,
      VK_NULL_HANDLE,
      0,
  };

  VkResult vkr = driver->vkCreateComputePipelines(
      driver->GetDev(), driver->GetShaderCache()->GetPipeCache(), 1, &compPipeInfo, NULL, pipe);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

static void create(WrappedVulkan *driver, const char *objName, const int line,
                   VkDescriptorSet *descSet, VkDescriptorPool pool, VkDescriptorSetLayout setLayout)
{
  VkDescriptorSetAllocateInfo descSetAllocInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, NULL, pool, 1, &setLayout,
  };

  // don't expect this to fail (or if it does then it should be immediately obvious, not transient).
  VkResult vkr = driver->vkAllocateDescriptorSets(driver->GetDev(), &descSetAllocInfo, descSet);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

// a simpler one-shot descriptor containing anything we might want to vary in a graphics pipeline
struct ConciseGraphicsPipeline
{
  // misc
  VkRenderPass renderPass;
  VkPipelineLayout pipeLayout;
  VkShaderModule vertex;
  VkShaderModule fragment;

  // dynamic state
  std::initializer_list<VkDynamicState> dynstates;

  // msaa
  VkSampleCountFlagBits sampleCount;
  bool sampleRateShading;

  // depth stencil
  bool depthEnable;
  bool stencilEnable;
  VkStencilOp stencilOp;

  // color blend
  bool colourOutput;
  bool blendEnable;
  VkBlendFactor srcBlend;
  VkBlendFactor dstBlend;
  uint32_t writeMask;
};

static void create(WrappedVulkan *driver, const char *objName, const int line, VkPipeline *pipe,
                   const ConciseGraphicsPipeline &info)
{
  // if the module didn't compile, this pipeline is not be supported. Silently don't create it, code
  // later should handle the missing pipeline as indicating lack of support
  if(info.vertex == VK_NULL_HANDLE || info.fragment == VK_NULL_HANDLE)
    return;

  // first configure the structs that contain parameters derived from the info parameter

  const VkPipelineShaderStageCreateInfo shaderStages[2] = {
      {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_VERTEX_BIT,
       info.vertex, "main", NULL},
      {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_FRAGMENT_BIT,
       info.fragment, "main", NULL},
  };

  const VkPipelineDynamicStateCreateInfo dynamicState = {
      VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
      NULL,
      0,
      (uint32_t)info.dynstates.size(),
      info.dynstates.begin(),
  };

  VkPipelineMultisampleStateCreateInfo msaa = {
      VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
  };

  msaa.rasterizationSamples = info.sampleCount;
  if(info.sampleRateShading)
  {
    msaa.minSampleShading = 1.0f;
    msaa.sampleShadingEnable = true;
  }

  const VkPipelineDepthStencilStateCreateInfo depthStencil = {
      VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
      NULL,
      0,
      info.depthEnable,
      info.depthEnable,
      VK_COMPARE_OP_ALWAYS,
      false,
      info.stencilEnable,
      {info.stencilOp, info.stencilOp, info.stencilOp, VK_COMPARE_OP_ALWAYS, 0xff, 0xff, 0},
      {info.stencilOp, info.stencilOp, info.stencilOp, VK_COMPARE_OP_ALWAYS, 0xff, 0xff, 0},
      0.0f,
      1.0f,
  };

  const VkPipelineColorBlendAttachmentState colAttach = {
      info.blendEnable,
      // colour blending
      info.srcBlend, info.dstBlend, VK_BLEND_OP_ADD,
      // alpha blending
      info.srcBlend, info.dstBlend, VK_BLEND_OP_ADD,
      // write mask
      info.writeMask,
  };

  const VkPipelineColorBlendStateCreateInfo colorBlend = {
      VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
      NULL,
      0,
      false,
      VK_LOGIC_OP_NO_OP,
      info.colourOutput ? 1U : 0U,
      &colAttach,
      {1.0f, 1.0f, 1.0f, 1.0f},
  };

  // below this point, structs are not affected by the info

  const VkPipelineVertexInputStateCreateInfo vertexInput = {
      VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
  };

  VkPipelineInputAssemblyStateCreateInfo inputAssembly = {
      VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
  };

  inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;

  VkPipelineViewportStateCreateInfo viewScissor = {
      VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
  viewScissor.viewportCount = viewScissor.scissorCount = 1;

  // add default scissor, if scissor is dynamic this will be ignored.
  VkRect2D scissor = {{0, 0}, {16384, 16384}};
  viewScissor.pScissors = &scissor;

  // can't really make a sensible one-size-fits-all default viewport like we can with scissors, so
  // make it small.
  VkViewport viewport = {0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
  viewScissor.pViewports = &viewport;

  VkPipelineRasterizationStateCreateInfo raster = {
      VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
  };

  raster.frontFace = VK_FRONT_FACE_CLOCKWISE;
  raster.lineWidth = 1.0f;

  const VkGraphicsPipelineCreateInfo graphicsPipeInfo = {
      VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
      NULL,
      0,
      2,
      shaderStages,
      &vertexInput,
      &inputAssembly,
      NULL,    // tess
      &viewScissor,
      &raster,
      &msaa,
      &depthStencil,
      &colorBlend,
      &dynamicState,
      info.pipeLayout,
      info.renderPass,
      0,                 // sub pass
      VK_NULL_HANDLE,    // base pipeline handle
      -1,                // base pipeline index
  };

  VkResult vkr = driver->vkCreateGraphicsPipelines(
      driver->GetDev(), driver->GetShaderCache()->GetPipeCache(), 1, &graphicsPipeInfo, NULL, pipe);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed creating object %s at line %i, vkr was %s", objName, line, ToStr(vkr).c_str());
}

// utility macro that lets us check for VkResult failures inside the utility helpers while
// preserving context from outside
#define CREATE_OBJECT(obj, ...) create(driver, #obj, __LINE__, &obj, __VA_ARGS__)

VulkanDebugManager::VulkanDebugManager(WrappedVulkan *driver)
{
  RenderDoc::Inst().RegisterMemoryRegion(this, sizeof(VulkanDebugManager));

  m_pDriver = driver;

  m_Device = m_pDriver->GetDev();
  VkDevice dev = m_Device;

  VulkanResourceManager *rm = driver->GetResourceManager();

  VkResult vkr = VK_SUCCESS;

  VulkanShaderCache *shaderCache = driver->GetShaderCache();

  //////////////////////////////////////////////////////////////////
  // Color MS <-> Buffer copy (via compute)
  VkDescriptorPoolSize bufferPoolTypes[] = {
      {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2},
      {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1},
      {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1},
  };

  VkDescriptorPoolCreateInfo bufferPoolInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
      NULL,
      0,
      1,
      ARRAY_COUNT(bufferPoolTypes),
      &bufferPoolTypes[0],
  };

  vkr = m_pDriver->vkCreateDescriptorPool(dev, &bufferPoolInfo, NULL, &m_BufferMSDescriptorPool);
  CheckVkResult(vkr);

  rm->SetInternalResource(GetResID(m_BufferMSDescriptorPool));

  CREATE_OBJECT(m_BufferMSDescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_ALL, NULL},
                    {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_ALL, NULL},
                    {2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  rm->SetInternalResource(GetResID(m_BufferMSDescSetLayout));

  CREATE_OBJECT(m_BufferMSPipeLayout, m_BufferMSDescSetLayout, sizeof(Vec4u) * 2);

  rm->SetInternalResource(GetResID(m_BufferMSPipeLayout));

  CREATE_OBJECT(m_MS2BufferPipe, m_BufferMSPipeLayout,
                shaderCache->GetBuiltinModule(BuiltinShader::MS2BufferCS));
  CREATE_OBJECT(m_DepthMS2BufferPipe, m_BufferMSPipeLayout,
                shaderCache->GetBuiltinModule(BuiltinShader::DepthMS2BufferCS));
  CREATE_OBJECT(m_Buffer2MSPipe, m_BufferMSPipeLayout,
                shaderCache->GetBuiltinModule(BuiltinShader::Buffer2MSCS));

  rm->SetInternalResource(GetResID(m_MS2BufferPipe));
  rm->SetInternalResource(GetResID(m_DepthMS2BufferPipe));
  rm->SetInternalResource(GetResID(m_Buffer2MSPipe));

  CREATE_OBJECT(m_BufferMSDescSet, m_BufferMSDescriptorPool, m_BufferMSDescSetLayout);
  rm->SetInternalResource(GetResID(m_BufferMSDescSet));

  //////////////////////////////////////////////////////////////////
  // Depth MS to Buffer copy (via compute)

  // need a dummy UINT texture to fill the binding when we don't have a stencil aspect to copy.
  // unfortunately there's no single guaranteed UINT format that can be sampled as MSAA, so we try a
  // few since hopefully we'll find one that will work.
  VkFormat attemptFormats[] = {VK_FORMAT_R8G8B8A8_UINT,     VK_FORMAT_R8_UINT,
                               VK_FORMAT_S8_UINT,           VK_FORMAT_D32_SFLOAT_S8_UINT,
                               VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT};

  for(VkFormat f : attemptFormats)
  {
    VkImageAspectFlags viewAspectMask =
        IsStencilFormat(f) ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
    VkImageAspectFlags barrierAspectMask = viewAspectMask;

    if(IsDepthAndStencilFormat(f) && (barrierAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT))
      barrierAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT;

    VkFormatProperties props = {};
    driver->vkGetPhysicalDeviceFormatProperties(driver->GetPhysDev(), f, &props);

    if(!(props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT))
      continue;

    VkImageCreateInfo imInfo = {
        VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
        NULL,
        0,
        VK_IMAGE_TYPE_2D,
        f,
        {1, 1, 1},
        1,
        1,
        VK_SAMPLE_COUNT_1_BIT,
        VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_SAMPLED_BIT,
        VK_SHARING_MODE_EXCLUSIVE,
        0,
        NULL,
        VK_IMAGE_LAYOUT_UNDEFINED,
    };

    VkImageFormatProperties imgprops = {};
    vkr = driver->vkGetPhysicalDeviceImageFormatProperties(driver->GetPhysDev(), f,
                                                           imInfo.imageType, imInfo.tiling,
                                                           imInfo.usage, imInfo.flags, &imgprops);

    if(vkr == VK_ERROR_FORMAT_NOT_SUPPORTED)
      continue;

    // if it doesn't support MSAA, bail out
    if(imgprops.sampleCounts == VK_SAMPLE_COUNT_1_BIT)
      continue;

    imInfo.samples = VK_SAMPLE_COUNT_2_BIT;

    // MoltenVK seems to only support 4/8 samples and not 2...
    if(imgprops.sampleCounts & VK_SAMPLE_COUNT_2_BIT)
      imInfo.samples = VK_SAMPLE_COUNT_2_BIT;
    else if(imgprops.sampleCounts & VK_SAMPLE_COUNT_4_BIT)
      imInfo.samples = VK_SAMPLE_COUNT_4_BIT;
    else if(imgprops.sampleCounts & VK_SAMPLE_COUNT_8_BIT)
      imInfo.samples = VK_SAMPLE_COUNT_8_BIT;
    else if(imgprops.sampleCounts & VK_SAMPLE_COUNT_16_BIT)
      imInfo.samples = VK_SAMPLE_COUNT_16_BIT;
    else if(imgprops.sampleCounts & VK_SAMPLE_COUNT_32_BIT)
      imInfo.samples = VK_SAMPLE_COUNT_32_BIT;
    else
      RDCWARN("Can't find supported MSAA sample count");

    RDCASSERT(imgprops.sampleCounts & imInfo.samples, imgprops.sampleCounts, imInfo.samples);

    vkr = driver->vkCreateImage(driver->GetDev(), &imInfo, NULL, &m_DummyStencilImage);
    CheckVkResult(vkr);

    NameVulkanObject(m_DummyStencilImage, "m_DummyStencilImage");

    rm->SetInternalResource(GetResID(m_DummyStencilImage));

    VkMemoryRequirements mrq = {};
    driver->vkGetImageMemoryRequirements(driver->GetDev(), m_DummyStencilImage, &mrq);

    // allocate memory
    VkMemoryAllocateInfo allocInfo = {
        VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size,
        driver->GetGPULocalMemoryIndex(mrq.memoryTypeBits),
    };

    vkr = driver->vkAllocateMemory(driver->GetDev(), &allocInfo, NULL, &m_DummyStencilMemory);
    CheckVkResult(vkr);

    if(vkr != VK_SUCCESS)
      return;

    rm->SetInternalResource(GetResID(m_DummyStencilMemory));

    vkr = driver->vkBindImageMemory(driver->GetDev(), m_DummyStencilImage, m_DummyStencilMemory, 0);
    CheckVkResult(vkr);

    VkImageViewCreateInfo viewInfo = {
        VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        NULL,
        0,
        m_DummyStencilImage,
        VK_IMAGE_VIEW_TYPE_2D_ARRAY,
        f,
        {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
         VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
        {
            viewAspectMask, 0, 1, 0, 1,
        },
    };

    vkr = driver->vkCreateImageView(driver->GetDev(), &viewInfo, NULL, &m_DummyStencilView);
    CheckVkResult(vkr);

    NameVulkanObject(m_DummyStencilView, "m_DummyStencilView");

    rm->SetInternalResource(GetResID(m_DummyStencilView));

    VkCommandBuffer cmd = driver->GetNextCmd();

    if(cmd == VK_NULL_HANDLE)
      return;

    VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                          VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

    vkr = ObjDisp(cmd)->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
    CheckVkResult(vkr);

    // need to update image layout into valid state
    VkImageMemoryBarrier barrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        NULL,
        0,
        VK_ACCESS_SHADER_READ_BIT,
        VK_IMAGE_LAYOUT_UNDEFINED,
        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        Unwrap(m_DummyStencilImage),
        {barrierAspectMask, 0, 1, 0, 1},
    };

    DoPipelineBarrier(cmd, 1, &barrier);

    ObjDisp(cmd)->EndCommandBuffer(Unwrap(cmd));

    break;
  }

  if(m_DummyStencilImage == VK_NULL_HANDLE)
  {
    RDCERR("Couldn't find any integer format we could generate a dummy multisampled image with");
  }

  VkFormat formats[] = {
      VK_FORMAT_D16_UNORM,         VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_X8_D24_UNORM_PACK32,
      VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT,        VK_FORMAT_D32_SFLOAT_S8_UINT,
  };

  VkSampleCountFlagBits sampleCounts[] = {
      VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT, VK_SAMPLE_COUNT_16_BIT,
  };

  RDCCOMPILE_ASSERT(ARRAY_COUNT(m_DepthArray2MSPipe) == ARRAY_COUNT(formats),
                    "Array count mismatch");
  RDCCOMPILE_ASSERT(ARRAY_COUNT(m_DepthArray2MSPipe[0]) == ARRAY_COUNT(sampleCounts),
                    "Array count mismatch");

  // we use VK_IMAGE_LAYOUT_GENERAL here because it matches the expected layout for the
  // non-depth copy, which uses a storage image.
  VkImageLayout rpLayout = VK_IMAGE_LAYOUT_GENERAL;

  for(size_t f = 0; f < ARRAY_COUNT(formats); f++)
  {
    // if the format isn't supported at all, bail out and don't try to create anything
    if(!(m_pDriver->GetFormatProperties(formats[f]).optimalTilingFeatures &
         VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT))
    {
      RDCDEBUG("Depth copies MSAA -> Array not supported for format %s", ToStr(formats[f]).c_str());
      continue;
    }

    if(!m_pDriver->GetDeviceEnabledFeatures().sampleRateShading)
    {
      RDCDEBUG("No depth Array -> MSAA copies can be supported without sample rate shading");
      continue;
    }

    ConciseGraphicsPipeline depthPipeInfo = {
        VK_NULL_HANDLE,
        m_BufferMSPipeLayout,
        shaderCache->GetBuiltinModule(BuiltinShader::BlitVS),
        shaderCache->GetBuiltinModule(BuiltinShader::DepthBuf2MSFS),
        {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_STENCIL_REFERENCE},
        VK_SAMPLE_COUNT_1_BIT,
        true,    // sampleRateShading
        true,    // depthEnable
        true,    // stencilEnable
        VK_STENCIL_OP_REPLACE,
        false,    // colourOutput
        false,    // blendEnable
        VK_BLEND_FACTOR_ONE,
        VK_BLEND_FACTOR_ZERO,
        0xf,    // writeMask
    };

    for(size_t s = 0; s < ARRAY_COUNT(sampleCounts); s++)
    {
      // if this sample count isn't supported, don't create it
      if(!(m_pDriver->GetDeviceProps().limits.framebufferDepthSampleCounts &
           (uint32_t)sampleCounts[s]))
      {
        RDCDEBUG("Depth copies Array -> MSAA not supported for sample count %u on format %s",
                 sampleCounts[s], ToStr(formats[f]).c_str());
        continue;
      }

      VkRenderPass depthArray2MSRP;

      CREATE_OBJECT(depthArray2MSRP, formats[f], sampleCounts[s], rpLayout);

      depthPipeInfo.renderPass = depthArray2MSRP;
      depthPipeInfo.sampleCount = sampleCounts[s];

      CREATE_OBJECT(m_DepthArray2MSPipe[f][s], depthPipeInfo);

      rm->SetInternalResource(GetResID(m_DepthArray2MSPipe[f][s]));

      m_pDriver->vkDestroyRenderPass(dev, depthArray2MSRP, NULL);
    }
  }

  if(RenderDoc::Inst().IsReplayApp())
  {
    VkDescriptorPoolSize descPoolTypes[] = {
        {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, ARRAY_COUNT(m_DiscardSet)},
    };

    VkDescriptorPoolCreateInfo descPoolInfo = {
        VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
        NULL,
        0,
        ARRAY_COUNT(m_DiscardSet),
        ARRAY_COUNT(descPoolTypes),
        &descPoolTypes[0],
    };

    // create descriptor pool
    vkr = driver->vkCreateDescriptorPool(driver->GetDev(), &descPoolInfo, NULL, &m_DiscardPool);
    CheckVkResult(vkr);

    CREATE_OBJECT(m_DiscardSetLayout,
                  {
                      {0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                  });

    CREATE_OBJECT(m_DiscardLayout, m_DiscardSetLayout, 4);

    ResourceFormat fmt;
    fmt.type = ResourceFormatType::Regular;
    fmt.compByteWidth = 4;
    fmt.compCount = 1;

    for(size_t i = 0; i < ARRAY_COUNT(m_DiscardSet); i++)
    {
      CREATE_OBJECT(m_DiscardSet[i], m_DiscardPool, m_DiscardSetLayout);

      fmt.compType = CompType::Float;
      bytebuf pattern = GetDiscardPattern(DiscardType(i), fmt);
      fmt.compType = CompType::UInt;
      pattern.append(GetDiscardPattern(DiscardType(i), fmt));

      m_DiscardCB[i].Create(m_pDriver, m_Device, pattern.size(), 1, 0);

      void *ptr = m_DiscardCB[i].Map();
      if(!ptr)
        return;
      memcpy(ptr, pattern.data(), pattern.size());
      m_DiscardCB[i].Unmap();

      VkDescriptorBufferInfo bufInfo = {};
      m_DiscardCB[i].FillDescriptor(bufInfo);

      VkWriteDescriptorSet writes[] = {
          {
              VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(m_DiscardSet[i]), 0, 0, 1,
              VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, NULL, &bufInfo, NULL,
          },
      };

      ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), ARRAY_COUNT(writes), writes, 0, NULL);
    }
  }

  // we only need this during replay, so don't create otherwise.
  if(RenderDoc::Inst().IsReplayApp())
  {
    m_ReadbackWindow.Create(driver, dev, STAGE_BUFFER_BYTE_SIZE, 1, GPUBuffer::eGPUBufferReadback);
  }
}

VulkanDebugManager::~VulkanDebugManager()
{
  VkDevice dev = m_Device;

  m_Custom.Destroy(m_pDriver);

  m_ReadbackWindow.Destroy();

  for(auto it = m_CachedMeshPipelines.begin(); it != m_CachedMeshPipelines.end(); ++it)
    for(uint32_t i = 0; i < VKMeshDisplayPipelines::ePipe_Count; i++)
      m_pDriver->vkDestroyPipeline(dev, it->second.pipes[i], NULL);

  m_pDriver->vkDestroyDescriptorPool(dev, m_BufferMSDescriptorPool, NULL);

  m_pDriver->vkDestroyImageView(dev, m_DummyStencilView, NULL);
  m_pDriver->vkDestroyImage(dev, m_DummyStencilImage, NULL);
  m_pDriver->vkFreeMemory(dev, m_DummyStencilMemory, NULL);

  m_pDriver->vkDestroyDescriptorSetLayout(dev, m_BufferMSDescSetLayout, NULL);
  m_pDriver->vkDestroyPipelineLayout(dev, m_BufferMSPipeLayout, NULL);
  m_pDriver->vkDestroyPipeline(dev, m_Buffer2MSPipe, NULL);

  m_pDriver->vkDestroyPipeline(dev, m_MS2BufferPipe, NULL);
  m_pDriver->vkDestroyPipeline(dev, m_DepthMS2BufferPipe, NULL);

  m_pDriver->vkDestroyDescriptorPool(dev, m_DiscardPool, NULL);
  m_pDriver->vkDestroyPipelineLayout(dev, m_DiscardLayout, NULL);
  m_pDriver->vkDestroyDescriptorSetLayout(dev, m_DiscardSetLayout, NULL);
  for(size_t i = 0; i < ARRAY_COUNT(m_DiscardCB); i++)
    m_DiscardCB[i].Destroy();

  for(auto it = m_DiscardImages.begin(); it != m_DiscardImages.end(); it++)
  {
    for(VkImageView view : it->second.views)
      m_pDriver->vkDestroyImageView(dev, view, NULL);
    for(VkFramebuffer fb : it->second.fbs)
      m_pDriver->vkDestroyFramebuffer(dev, fb, NULL);
  }

  for(auto it = m_DiscardPipes.begin(); it != m_DiscardPipes.end(); it++)
  {
    m_pDriver->vkDestroyPipeline(dev, it->second.pso, NULL);
    m_pDriver->vkDestroyRenderPass(dev, it->second.rp, NULL);
  }

  for(auto it = m_DiscardPatterns.begin(); it != m_DiscardPatterns.end(); it++)
    m_pDriver->vkDestroyBuffer(dev, it->second, NULL);
  for(auto it = m_DiscardStage.begin(); it != m_DiscardStage.end(); it++)
    it->second.Destroy();

  for(size_t f = 0; f < ARRAY_COUNT(m_DepthArray2MSPipe); f++)
    for(size_t s = 0; s < ARRAY_COUNT(m_DepthArray2MSPipe[0]); s++)
      m_pDriver->vkDestroyPipeline(dev, m_DepthArray2MSPipe[f][s], NULL);
}

void VulkanDebugManager::CreateCustomShaderTex(uint32_t width, uint32_t height, uint32_t mip)
{
  WrappedVulkan *driver = m_pDriver;

  VkDevice dev = m_Device;

  VkResult vkr = VK_SUCCESS;

  if(m_Custom.TexImg != VK_NULL_HANDLE)
  {
    if(width == m_Custom.TexWidth && height == m_Custom.TexHeight)
    {
      // recreate framebuffer for this mip
      m_pDriver->vkDestroyFramebuffer(dev, m_Custom.TexFB, NULL);

      // Create framebuffer rendering just to overlay image, no depth
      VkFramebufferCreateInfo fbinfo = {
          VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
          NULL,
          0,
          m_Custom.TexRP,
          1,
          &m_Custom.TexImgView[mip],
          RDCMAX(1U, width >> mip),
          RDCMAX(1U, height >> mip),
          1,
      };

      vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &m_Custom.TexFB);
      CheckVkResult(vkr);
      return;
    }

    m_pDriver->vkDestroyRenderPass(dev, m_Custom.TexRP, NULL);
    m_pDriver->vkDestroyFramebuffer(dev, m_Custom.TexFB, NULL);
    for(size_t i = 0; i < ARRAY_COUNT(m_Custom.TexImgView); i++)
      m_pDriver->vkDestroyImageView(dev, m_Custom.TexImgView[i], NULL);
    RDCEraseEl(m_Custom.TexImgView);
    m_pDriver->vkDestroyImage(dev, m_Custom.TexImg, NULL);
  }

  m_Custom.TexWidth = width;
  m_Custom.TexHeight = height;

  VkImageCreateInfo imInfo = {
      VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
      NULL,
      0,
      VK_IMAGE_TYPE_2D,
      VK_FORMAT_R16G16B16A16_SFLOAT,
      {width, height, 1},
      CalcNumMips((int)width, (int)height, 1),
      1,
      VK_SAMPLE_COUNT_1_BIT,
      VK_IMAGE_TILING_OPTIMAL,
      VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
          VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
      VK_SHARING_MODE_EXCLUSIVE,
      0,
      NULL,
      VK_IMAGE_LAYOUT_UNDEFINED,
  };

  vkr = m_pDriver->vkCreateImage(m_Device, &imInfo, NULL, &m_Custom.TexImg);
  CheckVkResult(vkr);

  NameVulkanObject(m_Custom.TexImg, "m_Custom.TexImg");

  VkMemoryRequirements mrq = {0};
  m_pDriver->vkGetImageMemoryRequirements(m_Device, m_Custom.TexImg, &mrq);

  // if no memory is allocated, or it's not enough,
  // then allocate
  if(m_Custom.TexMem == VK_NULL_HANDLE || mrq.size > m_Custom.TexMemSize)
  {
    if(m_Custom.TexMem != VK_NULL_HANDLE)
      m_pDriver->vkFreeMemory(m_Device, m_Custom.TexMem, NULL);

    VkMemoryAllocateInfo allocInfo = {
        VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size,
        m_pDriver->GetGPULocalMemoryIndex(mrq.memoryTypeBits),
    };

    vkr = m_pDriver->vkAllocateMemory(m_Device, &allocInfo, NULL, &m_Custom.TexMem);
    CheckVkResult(vkr);

    if(vkr != VK_SUCCESS)
      return;

    m_Custom.TexMemSize = mrq.size;
  }

  vkr = m_pDriver->vkBindImageMemory(m_Device, m_Custom.TexImg, m_Custom.TexMem, 0);
  CheckVkResult(vkr);

  VkImageViewCreateInfo viewInfo = {
      VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
      NULL,
      0,
      m_Custom.TexImg,
      VK_IMAGE_VIEW_TYPE_2D,
      imInfo.format,
      {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
       VK_COMPONENT_SWIZZLE_IDENTITY},
      {
          VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1,
      },
  };

  for(uint32_t i = 0; i < imInfo.mipLevels; i++)
  {
    viewInfo.subresourceRange.baseMipLevel = i;
    vkr = m_pDriver->vkCreateImageView(m_Device, &viewInfo, NULL, &m_Custom.TexImgView[i]);
    CheckVkResult(vkr);

    NameVulkanObject(m_Custom.TexImgView[i], "m_Custom.TexImgView[" + ToStr(i) + "]");
  }

  // need to update image layout into valid state

  VkCommandBuffer cmd = m_pDriver->GetNextCmd();

  if(cmd == VK_NULL_HANDLE)
    return;

  VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

  ObjDisp(dev)->BeginCommandBuffer(Unwrap(cmd), &beginInfo);

  m_pDriver->FindImageState(GetResID(m_Custom.TexImg))
      ->InlineTransition(cmd, m_pDriver->m_QueueFamilyIdx, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                         0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                         m_pDriver->GetImageTransitionInfo());

  vkr = ObjDisp(dev)->EndCommandBuffer(Unwrap(cmd));
  CheckVkResult(vkr);

#if ENABLED(SINGLE_FLUSH_VALIDATE)
  m_pDriver->SubmitCmds();
#endif

  CREATE_OBJECT(m_Custom.TexRP, imInfo.format, imInfo.samples);

  // Create framebuffer rendering just to overlay image, no depth
  VkFramebufferCreateInfo fbinfo = {
      VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
      NULL,
      0,
      m_Custom.TexRP,
      1,
      &m_Custom.TexImgView[mip],
      RDCMAX(1U, width >> mip),
      RDCMAX(1U, height >> mip),
      1,
  };

  vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &m_Custom.TexFB);
  CheckVkResult(vkr);
}

void VulkanDebugManager::CreateCustomShaderPipeline(ResourceId shader, VkPipelineLayout pipeLayout)
{
  WrappedVulkan *driver = m_pDriver;

  if(shader == ResourceId())
    return;

  if(m_Custom.TexPipeline != VK_NULL_HANDLE)
  {
    if(m_Custom.TexShader == shader)
      return;

    m_pDriver->vkDestroyPipeline(m_Device, m_Custom.TexPipeline, NULL);
  }

  m_Custom.TexShader = shader;

  ConciseGraphicsPipeline customPipe = {
      m_Custom.TexRP,
      pipeLayout,
      m_pDriver->GetShaderCache()->GetBuiltinModule(BuiltinShader::BlitVS),
      m_pDriver->GetResourceManager()->GetCurrentHandle<VkShaderModule>(shader),
      {VK_DYNAMIC_STATE_VIEWPORT},
      VK_SAMPLE_COUNT_1_BIT,
      false,    // sampleRateShading
      false,    // depthEnable
      false,    // stencilEnable
      VK_STENCIL_OP_KEEP,
      true,     // colourOutput
      false,    // blendEnable
      VK_BLEND_FACTOR_ONE,
      VK_BLEND_FACTOR_ZERO,
      0xf,    // writeMask
  };

  CREATE_OBJECT(m_Custom.TexPipeline, customPipe);
}

uint32_t VulkanReplay::PickVertex(uint32_t eventId, int32_t width, int32_t height,
                                  const MeshDisplay &cfg, uint32_t x, uint32_t y)
{
  VkDevice dev = m_pDriver->GetDev();
  const VkDevDispatchTable *vt = ObjDisp(dev);

  VkMarkerRegion::Begin(StringFormat::Fmt("VulkanReplay::PickVertex(%u, %u)", x, y));

  Matrix4f projMat = Matrix4f::Perspective(90.0f, 0.1f, 100000.0f, float(width) / float(height));

  Matrix4f camMat = cfg.cam ? ((Camera *)cfg.cam)->GetMatrix() : Matrix4f::Identity();
  Matrix4f pickMVP = projMat.Mul(camMat);
  if(!cfg.position.unproject)
  {
    pickMVP = pickMVP.Mul(Matrix4f(cfg.axisMapping));
  }

  bool reverseProjection = false;
  Matrix4f guessProj;
  Matrix4f guessProjInverse;
  if(cfg.position.unproject)
  {
    // the derivation of the projection matrix might not be right (hell, it could be an
    // orthographic projection). But it'll be close enough likely.
    if(cfg.position.farPlane != FLT_MAX)
    {
      guessProj =
          Matrix4f::Perspective(cfg.fov, cfg.position.nearPlane, cfg.position.farPlane, cfg.aspect);
    }
    else
    {
      reverseProjection = true;
      guessProj = Matrix4f::ReversePerspective(cfg.fov, cfg.position.nearPlane, cfg.aspect);
    }

    if(cfg.ortho)
      guessProj = Matrix4f::Orthographic(cfg.position.nearPlane, cfg.position.farPlane);

    if(cfg.position.flipY)
      guessProj[5] *= -1.0f;

    guessProjInverse = guessProj.Inverse();
  }

  Vec3f rayPos;
  Vec3f rayDir;
  // convert mouse pos to world space ray
  {
    float pickX = ((float)x) / ((float)width);
    float pickXCanonical = RDCLERP(-1.0f, 1.0f, pickX);

    float pickY = ((float)y) / ((float)height);
    // flip the Y axis by default for Y-up
    float pickYCanonical = RDCLERP(1.0f, -1.0f, pickY);

    if(cfg.position.flipY && !cfg.ortho)
      pickYCanonical = -pickYCanonical;

    // x/y is inside the window. Since we're not using the window projection we need to correct
    // for the aspect ratio here.
    if(cfg.position.unproject && !cfg.ortho)
      pickXCanonical *= (float(width) / float(height)) / cfg.aspect;

    // set up the NDC near/far pos
    Vec3f nearPosNDC = Vec3f(pickXCanonical, pickYCanonical, 0);
    Vec3f farPosNDC = Vec3f(pickXCanonical, pickYCanonical, 1);

    if(cfg.position.unproject && cfg.ortho)
    {
      // orthographic projections we raycast in NDC space
      Matrix4f inversePickMVP = pickMVP.Inverse();

      // transform from the desired NDC co-ordinates into camera space
      Vec3f nearPosCamera = inversePickMVP.Transform(nearPosNDC, 1);
      Vec3f farPosCamera = inversePickMVP.Transform(farPosNDC, 1);

      Vec3f testDir = (farPosCamera - nearPosCamera);
      testDir.Normalise();

      Matrix4f pickMVPguessProjInverse = guessProj.Mul(inversePickMVP);

      Vec3f nearPosProj = pickMVPguessProjInverse.Transform(nearPosNDC, 1);
      Vec3f farPosProj = pickMVPguessProjInverse.Transform(farPosNDC, 1);

      rayDir = (farPosProj - nearPosProj);
      rayDir.Normalise();

      // Calculate the ray direction first in the regular way (above), so we can use the
      // the output for testing if the ray we are picking is negative or not. This is similar
      // to checking against the forward direction of the camera, but more robust
      if(testDir.z < 0)
      {
        rayDir = -rayDir;
      }
      rayPos = nearPosProj;
    }
    else if(cfg.position.unproject)
    {
      // projected data we pick in world-space to avoid problems with handling unusual transforms

      if(reverseProjection)
      {
        farPosNDC.z = 1e-6f;
        nearPosNDC.z = 1e+6f;
      }

      // invert the guessed projection matrix to get the near/far pos in camera space
      Vec3f nearPosCamera = guessProjInverse.Transform(nearPosNDC, 1.0f);
      Vec3f farPosCamera = guessProjInverse.Transform(farPosNDC, 1.0f);

      // normalise and generate the ray
      rayDir = (farPosCamera - nearPosCamera);
      rayDir.Normalise();

      farPosCamera = nearPosCamera + rayDir;

      // invert the camera transform to transform the ray as camera-relative into world space
      Matrix4f inverseCamera = camMat.Inverse();

      Vec3f nearPosWorld = inverseCamera.Transform(nearPosCamera, 1);
      Vec3f farPosWorld = inverseCamera.Transform(farPosCamera, 1);

      // again normalise our final ray
      rayDir = (farPosWorld - nearPosWorld);
      rayDir.Normalise();

      rayPos = nearPosWorld;
    }
    else
    {
      Matrix4f inversePickMVP = pickMVP.Inverse();

      // transform from the desired NDC co-ordinates into model space
      Vec3f nearPosCamera = inversePickMVP.Transform(nearPosNDC, 1);
      Vec3f farPosCamera = inversePickMVP.Transform(farPosNDC, 1);

      rayDir = (farPosCamera - nearPosCamera);
      rayDir.Normalise();
      rayPos = nearPosCamera;
    }
  }

  const bool fandecode =
      (cfg.position.topology == Topology::TriangleFan && cfg.position.allowRestart);

  uint32_t numIndices = cfg.position.numIndices;

  bytebuf idxs;

  uint32_t minIndex = 0;
  uint32_t maxIndex = cfg.position.numIndices;

  if(cfg.position.indexByteStride && cfg.position.indexResourceId != ResourceId())
    GetBufferData(cfg.position.indexResourceId, cfg.position.indexByteOffset, 0, idxs);

  uint32_t idxclamp = 0;
  if(cfg.position.baseVertex < 0)
    idxclamp = uint32_t(-cfg.position.baseVertex);

  // We copy into our own buffers to promote to the target type (uint32) that the shader expects.
  // Most IBs will be 16-bit indices, most VBs will not be float4. We also apply baseVertex here

  if(!idxs.empty())
  {
    rdcarray<uint32_t> idxtmp;

    // if it's a triangle fan that allows restart, we'll have to unpack it.
    // Allocate enough space for the list on the GPU, and enough temporary space to upcast into
    // first
    if(fandecode)
    {
      idxtmp.resize(numIndices);

      numIndices *= 3;
    }

    // resize up on demand
    if(m_VertexPick.IBSize < numIndices * sizeof(uint32_t))
    {
      if(m_VertexPick.IBSize > 0)
      {
        m_VertexPick.IB.Destroy();
        m_VertexPick.IBUpload.Destroy();
      }

      m_VertexPick.IBSize = numIndices * sizeof(uint32_t);

      m_VertexPick.IB.Create(m_pDriver, dev, m_VertexPick.IBSize, 1,
                             GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
      m_VertexPick.IBUpload.Create(m_pDriver, dev, m_VertexPick.IBSize, 1, 0);
    }

    uint32_t *outidxs = (uint32_t *)m_VertexPick.IBUpload.Map();
    uint32_t *mappedPtr = outidxs;
    if(!mappedPtr)
      return ~0U;

    memset(outidxs, 0, m_VertexPick.IBSize);

    // if we're decoding a fan, we write into our temporary vector first
    if(fandecode)
      outidxs = idxtmp.data();

    uint16_t *idxs16 = (uint16_t *)&idxs[0];
    uint32_t *idxs32 = (uint32_t *)&idxs[0];

    size_t idxcount = 0;

    if(cfg.position.indexByteStride == 2)
    {
      size_t bufsize = idxs.size() / 2;

      for(uint32_t i = 0; i < bufsize && i < cfg.position.numIndices; i++)
      {
        uint32_t idx = idxs16[i];

        if(idx < idxclamp)
          idx = 0;
        else if(cfg.position.baseVertex < 0)
          idx -= idxclamp;
        else if(cfg.position.baseVertex > 0)
          idx += cfg.position.baseVertex;

        if(i == 0)
        {
          minIndex = maxIndex = idx;
        }
        else
        {
          minIndex = RDCMIN(idx, minIndex);
          maxIndex = RDCMAX(idx, maxIndex);
        }

        outidxs[i] = idx;
        idxcount++;
      }
    }
    else
    {
      uint32_t bufsize = uint32_t(idxs.size() / 4);

      minIndex = maxIndex = idxs32[0];

      for(uint32_t i = 0; i < RDCMIN(bufsize, cfg.position.numIndices); i++)
      {
        uint32_t idx = idxs32[i];

        if(idx < idxclamp)
          idx = 0;
        else if(cfg.position.baseVertex < 0)
          idx -= idxclamp;
        else if(cfg.position.baseVertex > 0)
          idx += cfg.position.baseVertex;

        minIndex = RDCMIN(idx, minIndex);
        maxIndex = RDCMAX(idx, maxIndex);

        outidxs[i] = idx;
        idxcount++;
      }
    }

    // if it's a triangle fan that allows restart, unpack it
    if(cfg.position.topology == Topology::TriangleFan && cfg.position.allowRestart)
    {
      // resize to how many indices were actually read
      idxtmp.resize(idxcount);

      // patch the index buffer
      PatchTriangleFanRestartIndexBufer(idxtmp, cfg.position.restartIndex);

      for(uint32_t &idx : idxtmp)
      {
        if(idx == cfg.position.restartIndex)
          idx = 0;
      }

      numIndices = (uint32_t)idxtmp.size();

      // now copy the decoded list to the GPU
      memcpy(mappedPtr, idxtmp.data(), idxtmp.size() * sizeof(uint32_t));
    }

    m_VertexPick.IBUpload.Unmap();
  }
  else
  {
    // ensure IB is non-empty so we have a valid descriptor below
    if(m_VertexPick.IBSize == 0)
    {
      m_VertexPick.IBSize = 1 * sizeof(uint32_t);

      m_VertexPick.IB.Create(m_pDriver, dev, m_VertexPick.IBSize, 1,
                             GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
    }
  }

  // unpack and linearise the data
  {
    bytebuf oldData;
    GetBufferData(cfg.position.vertexResourceId, cfg.position.vertexByteOffset, 0, oldData);

    // clamp maxIndex to upper bound in case we got invalid indices or primitive restart indices
    maxIndex = RDCMIN(maxIndex, uint32_t(oldData.size() / RDCMAX(1U, cfg.position.vertexByteStride)));

    if(m_VertexPick.VBSize < (maxIndex + 1) * sizeof(FloatVector))
    {
      if(m_VertexPick.VBSize > 0)
      {
        m_VertexPick.VB.Destroy();
        m_VertexPick.VBUpload.Destroy();
      }

      m_VertexPick.VBSize = (maxIndex + 1) * sizeof(FloatVector);

      m_VertexPick.VB.Create(m_pDriver, dev, m_VertexPick.VBSize, 1,
                             GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
      m_VertexPick.VBUpload.Create(m_pDriver, dev, m_VertexPick.VBSize, 1, 0);
    }

    byte *data = &oldData[0];
    byte *dataEnd = data + oldData.size();

    bool valid = true;

    FloatVector *vbData = (FloatVector *)m_VertexPick.VBUpload.Map();
    if(!vbData)
      return ~0U;

    // the index buffer may refer to vertices past the start of the vertex buffer, so we can't just
    // conver the first N vertices we'll need.
    // Instead we grab min and max above, and convert every vertex in that range. This might
    // slightly over-estimate but not as bad as 0-max or the whole buffer.
    for(uint32_t idx = minIndex; idx <= maxIndex; idx++)
      vbData[idx] = HighlightCache::InterpretVertex(data, idx, cfg.position.vertexByteStride,
                                                    cfg.position.format, dataEnd, valid);

    m_VertexPick.VBUpload.Unmap();
  }

  MeshPickUBOData *ubo = (MeshPickUBOData *)m_VertexPick.UBO.Map();
  if(!ubo)
    return ~0U;

  ubo->rayPos = rayPos;
  ubo->rayDir = rayDir;
  ubo->use_indices = cfg.position.indexByteStride ? 1U : 0U;
  ubo->numVerts = numIndices;
  bool isTriangleMesh = true;

  switch(cfg.position.topology)
  {
    case Topology::TriangleList:
    {
      ubo->meshMode = MESH_TRIANGLE_LIST;
      break;
    };
    case Topology::TriangleStrip:
    {
      ubo->meshMode = MESH_TRIANGLE_STRIP;
      break;
    };
    case Topology::TriangleFan:
    {
      if(fandecode)
        ubo->meshMode = MESH_TRIANGLE_LIST;
      else
        ubo->meshMode = MESH_TRIANGLE_FAN;
      break;
    };
    case Topology::TriangleList_Adj:
    {
      ubo->meshMode = MESH_TRIANGLE_LIST_ADJ;
      break;
    };
    case Topology::TriangleStrip_Adj:
    {
      ubo->meshMode = MESH_TRIANGLE_STRIP_ADJ;
      break;
    };
    default:    // points, lines, patchlists, unknown
    {
      ubo->meshMode = MESH_OTHER;
      isTriangleMesh = false;
    };
  }

  // line/point data
  ubo->unproject = cfg.position.unproject;
  ubo->flipY = cfg.position.flipY;
  ubo->ortho = cfg.ortho;
  ubo->coords = Vec2f((float)x, (float)y);
  ubo->viewport = Vec2f((float)width, (float)height);

  if(cfg.position.unproject && isTriangleMesh)
  {
    // projected triangle meshes we transform the vertices into world space, and ray-cast against
    // that
    //
    // NOTE: for ortho, this matrix is not used and we just do the perspective W division on model
    // vertices. The ray is cast in NDC
    if(cfg.ortho)
      ubo->transformMat = Matrix4f::Identity();
    else
      ubo->transformMat = guessProjInverse;
  }
  else if(cfg.position.unproject)
  {
    // projected non-triangles are just point clouds, so we transform the vertices into world space
    // then project them back onto the output and compare that against the picking 2D co-ordinates
    ubo->transformMat = pickMVP.Mul(guessProjInverse);
  }
  else
  {
    // plain meshes of either type, we just transform from model space to the output, and raycast or
    // co-ordinate check
    ubo->transformMat = pickMVP;
  }

  m_VertexPick.UBO.Unmap();

  VkDescriptorBufferInfo ibInfo = {};
  VkDescriptorBufferInfo vbInfo = {};

  m_VertexPick.VB.FillDescriptor(vbInfo);
  m_VertexPick.IB.FillDescriptor(ibInfo);

  VkWriteDescriptorSet writes[] = {
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(m_VertexPick.DescSet), 1, 0, 1,
       VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NULL, &vbInfo, NULL},
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(m_VertexPick.DescSet), 2, 0, 1,
       VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NULL, &ibInfo, NULL},
  };

  vt->UpdateDescriptorSets(Unwrap(m_Device), 2, writes, 0, NULL);

  VkCommandBuffer cmd = m_pDriver->GetNextCmd();

  if(cmd == VK_NULL_HANDLE)
    return ~0U;

  VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

  VkBufferCopy bufCopy = {0, 0, 0};

  vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);

  // reset first uint (used as atomic counter) to 0
  vt->CmdFillBuffer(Unwrap(cmd), Unwrap(m_VertexPick.Result.buf), 0, sizeof(uint32_t) * 4, 0);

  VkBufferMemoryBarrier bufBarrier = {
      VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
      NULL,
      VK_ACCESS_TRANSFER_WRITE_BIT,
      VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT,
      VK_QUEUE_FAMILY_IGNORED,
      VK_QUEUE_FAMILY_IGNORED,
      Unwrap(m_VertexPick.Result.buf),
      0,
      VK_WHOLE_SIZE,
  };

  // wait for zero to be written to atomic counter before using in shader
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  // copy uploaded VB and if needed IB
  if(!idxs.empty())
  {
    // wait for writes
    bufBarrier.buffer = Unwrap(m_VertexPick.IBUpload.buf);
    bufBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
    bufBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
    DoPipelineBarrier(cmd, 1, &bufBarrier);

    // do copy
    bufCopy.size = m_VertexPick.IBSize;
    vt->CmdCopyBuffer(Unwrap(cmd), Unwrap(m_VertexPick.IBUpload.buf), Unwrap(m_VertexPick.IB.buf),
                      1, &bufCopy);

    // wait for copy
    bufBarrier.buffer = Unwrap(m_VertexPick.IB.buf);
    bufBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    bufBarrier.dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT;
    DoPipelineBarrier(cmd, 1, &bufBarrier);
  }

  // wait for writes
  bufBarrier.buffer = Unwrap(m_VertexPick.VBUpload.buf);
  bufBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
  bufBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  // do copy
  bufCopy.size = m_VertexPick.VBSize;
  vt->CmdCopyBuffer(Unwrap(cmd), Unwrap(m_VertexPick.VBUpload.buf), Unwrap(m_VertexPick.VB.buf), 1,
                    &bufCopy);

  // wait for copy
  bufBarrier.buffer = Unwrap(m_VertexPick.VB.buf);
  bufBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  bufBarrier.dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT;
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_COMPUTE, Unwrap(m_VertexPick.Pipeline));
  vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_COMPUTE, Unwrap(m_VertexPick.Layout),
                            0, 1, UnwrapPtr(m_VertexPick.DescSet), 0, NULL);

  uint32_t workgroupx = uint32_t(cfg.position.numIndices / 128 + 1);
  vt->CmdDispatch(Unwrap(cmd), workgroupx, 1, 1);

  // wait for shader to finish writing before transferring to readback buffer
  bufBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
  bufBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
  bufBarrier.buffer = Unwrap(m_VertexPick.Result.buf);
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  bufCopy.size = m_VertexPick.Result.totalsize;

  // copy to readback buffer
  vt->CmdCopyBuffer(Unwrap(cmd), Unwrap(m_VertexPick.Result.buf),
                    Unwrap(m_VertexPick.ResultReadback.buf), 1, &bufCopy);

  // wait for transfer to finish before reading on CPU
  bufBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  bufBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
  bufBarrier.buffer = Unwrap(m_VertexPick.ResultReadback.buf);
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  VkResult vkr = vt->EndCommandBuffer(Unwrap(cmd));
  CheckVkResult(vkr);

#if ENABLED(SINGLE_FLUSH_VALIDATE)
  m_pDriver->SubmitCmds();
#endif

  m_pDriver->SubmitCmds();
  m_pDriver->FlushQ();

  uint32_t *pickResultData = (uint32_t *)m_VertexPick.ResultReadback.Map();
  uint32_t numResults = *pickResultData;
  if(!pickResultData)
    return ~0U;

  uint32_t ret = ~0U;

  if(numResults > 0)
  {
    if(isTriangleMesh)
    {
      struct PickResult
      {
        uint32_t vertid;
        vec3 intersectionPoint;
      };

      PickResult *pickResults = (PickResult *)(pickResultData + 4);

      PickResult *closest = pickResults;
      // distance from raycast hit to nearest worldspace position of the mouse
      float closestPickDistance = (closest->intersectionPoint - rayPos).Length();

      // min with size of results buffer to protect against overflows
      for(uint32_t i = 1; i < RDCMIN((uint32_t)VertexPicking::MaxMeshPicks, numResults); i++)
      {
        float pickDistance = (pickResults[i].intersectionPoint - rayPos).Length();
        if(pickDistance < closestPickDistance)
        {
          closest = pickResults + i;
        }
      }
      ret = closest->vertid;
    }
    else
    {
      struct PickResult
      {
        uint32_t vertid;
        uint32_t idx;
        float len;
        float depth;
      };

      PickResult *pickResults = (PickResult *)(pickResultData + 4);

      PickResult *closest = pickResults;

      // min with size of results buffer to protect against overflows
      for(uint32_t i = 1; i < RDCMIN((uint32_t)VertexPicking::MaxMeshPicks, numResults); i++)
      {
        // We need to keep the picking order consistent in the face
        // of random buffer appends, when multiple vertices have the
        // identical position (e.g. if UVs or normals are different).
        //
        // We could do something to try and disambiguate, but it's
        // never going to be intuitive, it's just going to flicker
        // confusingly.
        if(pickResults[i].len < closest->len ||
           (pickResults[i].len == closest->len && pickResults[i].depth < closest->depth) ||
           (pickResults[i].len == closest->len && pickResults[i].depth == closest->depth &&
            pickResults[i].vertid < closest->vertid))
          closest = pickResults + i;
      }
      ret = closest->vertid;
    }
  }

  m_VertexPick.ResultReadback.Unmap();

  VkMarkerRegion::Set(StringFormat::Fmt("Result is %u", ret));

  VkMarkerRegion::End();

  if(fandecode)
  {
    // undo the triangle list expansion
    if(ret > 2)
      ret = (ret + 3) / 3 + 1;
  }

  return ret;
}

const VulkanCreationInfo::Image &VulkanDebugManager::GetImageInfo(ResourceId img) const
{
  auto it = m_pDriver->m_CreationInfo.m_Image.find(img);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_Image.end());
  return it->second;
}

const VulkanCreationInfo::ImageView &VulkanDebugManager::GetImageViewInfo(ResourceId imgView) const
{
  auto it = m_pDriver->m_CreationInfo.m_ImageView.find(imgView);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_ImageView.end());
  return it->second;
}

const VulkanCreationInfo::Pipeline &VulkanDebugManager::GetPipelineInfo(ResourceId pipe) const
{
  auto it = m_pDriver->m_CreationInfo.m_Pipeline.find(pipe);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_Pipeline.end());
  return it->second;
}

const VulkanCreationInfo::ShaderModule &VulkanDebugManager::GetShaderInfo(ResourceId shader) const
{
  auto it = m_pDriver->m_CreationInfo.m_ShaderModule.find(shader);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_ShaderModule.end());
  return it->second;
}

const VulkanCreationInfo::Framebuffer &VulkanDebugManager::GetFramebufferInfo(ResourceId fb) const
{
  auto it = m_pDriver->m_CreationInfo.m_Framebuffer.find(fb);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_Framebuffer.end());
  return it->second;
}

const VulkanCreationInfo::RenderPass &VulkanDebugManager::GetRenderPassInfo(ResourceId rp) const
{
  auto it = m_pDriver->m_CreationInfo.m_RenderPass.find(rp);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_RenderPass.end());
  return it->second;
}

const VulkanCreationInfo::PipelineLayout &VulkanDebugManager::GetPipelineLayoutInfo(ResourceId rp) const
{
  auto it = m_pDriver->m_CreationInfo.m_PipelineLayout.find(rp);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_PipelineLayout.end());
  return it->second;
}

const DescSetLayout &VulkanDebugManager::GetDescSetLayout(ResourceId dsl) const
{
  auto it = m_pDriver->m_CreationInfo.m_DescSetLayout.find(dsl);
  RDCASSERT(it != m_pDriver->m_CreationInfo.m_DescSetLayout.end());
  return it->second;
}

const WrappedVulkan::DescriptorSetInfo &VulkanDebugManager::GetDescSetInfo(ResourceId ds) const
{
  auto it = m_pDriver->m_DescriptorSetState.find(ds);
  RDCASSERT(it != m_pDriver->m_DescriptorSetState.end());
  return it->second;
}

void VulkanDebugManager::GetBufferData(ResourceId buff, uint64_t offset, uint64_t len, bytebuf &ret)
{
  VkDevice dev = m_pDriver->GetDev();
  const VkDevDispatchTable *vt = ObjDisp(dev);

  if(!m_pDriver->GetResourceManager()->HasCurrentResource(buff))
  {
    RDCERR("Getting buffer data for unknown buffer/memory %s!", ToStr(buff).c_str());
    return;
  }

  WrappedVkRes *res = m_pDriver->GetResourceManager()->GetCurrentResource(buff);

  if(res == VK_NULL_HANDLE)
  {
    RDCERR("Getting buffer data for unknown buffer/memory %s!", ToStr(buff).c_str());
    return;
  }

  VkBuffer srcBuf = VK_NULL_HANDLE;
  uint64_t bufsize = 0;

  if(WrappedVkDeviceMemory::IsAlloc(res))
  {
    srcBuf = m_pDriver->m_CreationInfo.m_Memory[buff].wholeMemBuf;
    bufsize = m_pDriver->m_CreationInfo.m_Memory[buff].wholeMemBufSize;

    if(srcBuf == VK_NULL_HANDLE)
    {
      RDCLOG(
          "Memory doesn't have wholeMemBuf, either non-buffer accessible (non-linear) or dedicated "
          "image memory");
      return;
    }
  }
  else if(WrappedVkBuffer::IsAlloc(res))
  {
    srcBuf = m_pDriver->GetResourceManager()->GetCurrentHandle<VkBuffer>(buff);
    bufsize = m_pDriver->m_CreationInfo.m_Buffer[buff].size;
  }
  else
  {
    RDCERR("Getting buffer data for object that isn't buffer or memory %s!", ToStr(buff).c_str());
    return;
  }

  if(offset >= bufsize)
  {
    // can't read past the end of the buffer, return empty
    return;
  }

  if(len == 0 || len > bufsize)
  {
    len = bufsize - offset;
  }

  if(VkDeviceSize(offset + len) > bufsize)
  {
    RDCWARN("Attempting to read off the end of the buffer (%llu %llu). Will be clamped (%llu)",
            offset, len, bufsize);
    len = RDCMIN(len, bufsize - offset);
  }

  ret.resize((size_t)len);

  VkDeviceSize srcoffset = (VkDeviceSize)offset;
  size_t dstoffset = 0;
  VkDeviceSize sizeRemaining = (VkDeviceSize)len;

  VkCommandBuffer cmd = m_pDriver->GetNextCmd();

  if(cmd == VK_NULL_HANDLE)
    return;

  VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

  VkResult vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
  CheckVkResult(vkr);

  VkBufferMemoryBarrier bufBarrier = {
      VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
      NULL,
      0,
      VK_ACCESS_TRANSFER_READ_BIT,
      VK_QUEUE_FAMILY_IGNORED,
      VK_QUEUE_FAMILY_IGNORED,
      Unwrap(srcBuf),
      srcoffset,
      sizeRemaining,
  };

  bufBarrier.srcAccessMask = VK_ACCESS_ALL_WRITE_BITS;

  // wait for previous writes to happen before we copy to our window buffer
  DoPipelineBarrier(cmd, 1, &bufBarrier);

  vkr = vt->EndCommandBuffer(Unwrap(cmd));
  CheckVkResult(vkr);

#if ENABLED(SINGLE_FLUSH_VALIDATE)
  m_pDriver->SubmitCmds();
#endif

  while(sizeRemaining > 0)
  {
    VkDeviceSize chunkSize = RDCMIN(sizeRemaining, STAGE_BUFFER_BYTE_SIZE);

    cmd = m_pDriver->GetNextCmd();

    if(cmd == VK_NULL_HANDLE)
      return;

    vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
    CheckVkResult(vkr);

    VkBufferCopy region = {srcoffset, 0, chunkSize};
    vt->CmdCopyBuffer(Unwrap(cmd), Unwrap(srcBuf), Unwrap(m_ReadbackWindow.buf), 1, &region);

    bufBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    bufBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
    bufBarrier.buffer = Unwrap(m_ReadbackWindow.buf);
    bufBarrier.offset = 0;
    bufBarrier.size = chunkSize;

    // wait for transfer to happen before we read
    DoPipelineBarrier(cmd, 1, &bufBarrier);

    vkr = vt->EndCommandBuffer(Unwrap(cmd));
    CheckVkResult(vkr);

    m_pDriver->SubmitCmds();
    m_pDriver->FlushQ();

    byte *pData = NULL;
    vkr = vt->MapMemory(Unwrap(dev), Unwrap(m_ReadbackWindow.mem), 0, VK_WHOLE_SIZE, 0,
                        (void **)&pData);
    CheckVkResult(vkr);
    if(vkr != VK_SUCCESS)
      return;
    if(!pData)
    {
      RDCERR("Manually reporting failed memory map");
      CheckVkResult(VK_ERROR_MEMORY_MAP_FAILED);
      return;
    }

    VkMappedMemoryRange range = {
        VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, Unwrap(m_ReadbackWindow.mem), 0, VK_WHOLE_SIZE,
    };

    vkr = vt->InvalidateMappedMemoryRanges(Unwrap(dev), 1, &range);
    CheckVkResult(vkr);

    RDCASSERT(pData != NULL);
    memcpy(&ret[dstoffset], pData, (size_t)chunkSize);

    srcoffset += chunkSize;
    dstoffset += (size_t)chunkSize;
    sizeRemaining -= chunkSize;

    vt->UnmapMemory(Unwrap(dev), Unwrap(m_ReadbackWindow.mem));
  }

  vt->DeviceWaitIdle(Unwrap(dev));
}

void VulkanDebugManager::FillWithDiscardPattern(VkCommandBuffer cmd, DiscardType type,
                                                VkImage image, VkImageLayout curLayout,
                                                VkImageSubresourceRange discardRange,
                                                VkRect2D discardRect)
{
  VkDevice dev = m_Device;
  const VkDevDispatchTable *vt = ObjDisp(dev);
  const VulkanCreationInfo::Image &imInfo = GetImageInfo(GetResID(image));

  VkMarkerRegion marker(
      cmd, StringFormat::Fmt("FillWithDiscardPattern %s", ToStr(GetResID(image)).c_str()));

  VkImageAspectFlags imAspects = FormatImageAspects(imInfo.format);

  VkImageSubresourceRange barrierDiscardRange = discardRange;
  barrierDiscardRange.aspectMask = imAspects;

  if(imInfo.samples > 1)
  {
    WrappedVulkan *driver = m_pDriver;

    bool depth = false;
    if(IsDepthOrStencilFormat(imInfo.format))
      depth = true;

    rdcpair<VkFormat, VkSampleCountFlagBits> key = {imInfo.format, imInfo.samples};

    DiscardPassData &passdata = m_DiscardPipes[key];

    // create and cache a pipeline and RP that writes to this format and sample count
    if(passdata.pso == VK_NULL_HANDLE)
    {
      BuiltinShaderBaseType baseType = BuiltinShaderBaseType::Float;

      if(IsSIntFormat(imInfo.format))
        baseType = BuiltinShaderBaseType::SInt;
      else if(IsUIntFormat(imInfo.format))
        baseType = BuiltinShaderBaseType::UInt;

      VkAttachmentReference attRef = {
          0, depth ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
                   : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
      };

      VkAttachmentDescription attDesc = {
          0,
          imInfo.format,
          imInfo.samples,
          VK_ATTACHMENT_LOAD_OP_LOAD,
          VK_ATTACHMENT_STORE_OP_STORE,
          VK_ATTACHMENT_LOAD_OP_LOAD,
          VK_ATTACHMENT_STORE_OP_STORE,
          attRef.layout,
          attRef.layout,
      };

      VkSubpassDescription sub = {
          0, VK_PIPELINE_BIND_POINT_GRAPHICS,
      };

      if(depth)
      {
        sub.pDepthStencilAttachment = &attRef;
      }
      else
      {
        sub.pColorAttachments = &attRef;
        sub.colorAttachmentCount = 1;
      }

      VkRenderPassCreateInfo rpinfo = {
          VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, NULL, 0, 1, &attDesc, 1, &sub, 0, NULL,
      };

      VkResult vkr = m_pDriver->vkCreateRenderPass(m_pDriver->GetDev(), &rpinfo, NULL, &passdata.rp);
      if(vkr != VK_SUCCESS)
        RDCERR("Failed to create shader debug render pass: %s", ToStr(vkr).c_str());

      ConciseGraphicsPipeline pipeInfo = {
          passdata.rp,
          m_DiscardLayout,
          m_pDriver->GetShaderCache()->GetBuiltinModule(BuiltinShader::BlitVS),
          m_pDriver->GetShaderCache()->GetBuiltinModule(BuiltinShader::DiscardFS, baseType),
          {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_REFERENCE},
          imInfo.samples,
          false,    // sampleRateShading
          true,     // depthEnable
          true,     // stencilEnable
          VK_STENCIL_OP_REPLACE,
          true,     // colourOutput
          false,    // blendEnable
          VK_BLEND_FACTOR_ONE,
          VK_BLEND_FACTOR_ZERO,
          0xf,    // writeMask
      };

      CREATE_OBJECT(passdata.pso, pipeInfo);
    }

    if(passdata.pso == VK_NULL_HANDLE)
      return;

    DiscardImgData &imgdata = m_DiscardImages[GetResID(image)];

    // create and cache views and framebuffers for every slice in this image
    if(imgdata.fbs.empty())
    {
      VkImageAspectFlags aspectMask = imAspects;

      for(int pass = 0; pass < 3; pass++)
      {
        // only depth/stencil images need multiple sets of views to mask out one aspect or the other
        if(pass > 0)
        {
          if(imAspects != (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT))
            break;

          if(pass == 1)
            aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
          else if(pass == 2)
            aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
        }

        for(uint32_t a = 0; a < imInfo.arrayLayers; a++)
        {
          VkImageViewCreateInfo viewInfo = {
              VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
              NULL,
              0,
              image,
              VK_IMAGE_VIEW_TYPE_2D,
              imInfo.format,
              {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
               VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
              {
                  aspectMask, 0, 1, a, 1,
              },
          };

          VkImageView view;
          VkResult vkr = driver->vkCreateImageView(driver->GetDev(), &viewInfo, NULL, &view);
          CheckVkResult(vkr);
          NameVulkanObject(view, StringFormat::Fmt("FillWithDiscardPattern view %s",
                                                   ToStr(GetResID(image)).c_str()));

          imgdata.views.push_back(view);

          // create framebuffer
          VkFramebufferCreateInfo fbinfo = {
              VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
              NULL,
              0,
              passdata.rp,
              1,
              &view,
              imInfo.extent.width,
              imInfo.extent.height,
              1,
          };

          VkFramebuffer fb;
          vkr = driver->vkCreateFramebuffer(driver->GetDev(), &fbinfo, NULL, &fb);
          CheckVkResult(vkr);

          imgdata.fbs.push_back(fb);
        }
      }
    }

    if(imgdata.fbs.empty())
      return;

    ObjDisp(cmd)->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS, Unwrap(passdata.pso));
    ObjDisp(cmd)->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                        Unwrap(m_DiscardLayout), 0, 1,
                                        UnwrapPtr(m_DiscardSet[(size_t)type]), 0, NULL);
    VkViewport viewport = {0.0f, 0.0f, (float)imInfo.extent.width, (float)imInfo.extent.height, 1.0f};
    ObjDisp(cmd)->CmdSetViewport(Unwrap(cmd), 0, 1U, &viewport);
    ObjDisp(cmd)->CmdSetScissor(Unwrap(cmd), 0, 1U, &discardRect);

    discardRect.extent.width =
        RDCMIN(discardRect.extent.width, imInfo.extent.width - discardRect.offset.x);
    discardRect.extent.height =
        RDCMIN(discardRect.extent.height, imInfo.extent.height - discardRect.offset.y);

    discardRange.layerCount =
        RDCMIN(discardRange.layerCount, imInfo.arrayLayers - discardRange.baseArrayLayer);

    VkRenderPassBeginInfo rpbegin = {
        VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
        NULL,
        Unwrap(passdata.rp),
        VK_NULL_HANDLE,
        discardRect,
    };

    uint32_t pass = 0;

    VkImageMemoryBarrier dstimBarrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, NULL,
        VK_ACCESS_ALL_READ_BITS | VK_ACCESS_ALL_WRITE_BITS,
        depth ? (VkAccessFlags)VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
              : (VkAccessFlags)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        curLayout, depth ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
                         : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, Unwrap(image), barrierDiscardRange,
    };

    DoPipelineBarrier(cmd, 1, &dstimBarrier);

    ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DiscardLayout), VK_SHADER_STAGE_ALL, 0, 4,
                                   &pass);

    uint32_t offset = 0;
    if(imAspects != discardRange.aspectMask)
    {
      // if we're only discarding one of depth or stencil in a depth/stencil image, pick a
      // framebuffer that only targets that aspect.
      if(discardRange.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT)
        offset = imInfo.arrayLayers;
      else
        offset = imInfo.arrayLayers * 2;
    }

    for(uint32_t slice = discardRange.baseArrayLayer;
        slice < discardRange.baseArrayLayer + discardRange.layerCount; slice++)
    {
      rpbegin.framebuffer = Unwrap(imgdata.fbs[slice + offset]);
      ObjDisp(cmd)->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

      if(depth && discardRange.aspectMask != VK_IMAGE_ASPECT_DEPTH_BIT)
      {
        pass = 1;
        ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DiscardLayout), VK_SHADER_STAGE_ALL, 0,
                                       4, &pass);
        ObjDisp(cmd)->CmdSetStencilReference(
            Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT | VK_STENCIL_FACE_BACK_BIT, 0x00);
        ObjDisp(cmd)->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);

        pass = 2;
        ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DiscardLayout), VK_SHADER_STAGE_ALL, 0,
                                       4, &pass);
        ObjDisp(cmd)->CmdSetStencilReference(
            Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT | VK_STENCIL_FACE_BACK_BIT, 0xff);
        ObjDisp(cmd)->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
      }
      else
      {
        ObjDisp(cmd)->CmdSetStencilReference(
            Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT | VK_STENCIL_FACE_BACK_BIT, 0x00);
        ObjDisp(cmd)->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
      }

      ObjDisp(cmd)->CmdEndRenderPass(Unwrap(cmd));
    }

    dstimBarrier.oldLayout = dstimBarrier.newLayout;
    dstimBarrier.newLayout = curLayout;
    dstimBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    dstimBarrier.dstAccessMask = VK_ACCESS_ALL_WRITE_BITS | VK_ACCESS_ALL_READ_BITS;

    DoPipelineBarrier(cmd, 1, &dstimBarrier);

    m_pDriver->GetCmdRenderState().BindPipeline(m_pDriver, cmd, VulkanRenderState::BindInitial,
                                                false);

    return;
  }

  rdcpair<VkFormat, DiscardType> key = {imInfo.format, type};

  if(key.first == VK_FORMAT_S8_UINT)
    key.first = VK_FORMAT_D32_SFLOAT_S8_UINT;

  VkBuffer buf = m_DiscardPatterns[key];
  VkResult vkr = VK_SUCCESS;

  static const uint32_t PatternBatchWidth = 256;
  static const uint32_t PatternBatchHeight = 256;

  VkImageAspectFlags aspectFlags = discardRange.aspectMask & FormatImageAspects(imInfo.format);

  if(buf == VK_NULL_HANDLE)
  {
    GPUBuffer &stage = m_DiscardStage[key];
    bytebuf pattern = GetDiscardPattern(key.second, MakeResourceFormat(key.first));

    BlockShape shape = GetBlockShape(key.first, 0);

    if(key.first == VK_FORMAT_D32_SFLOAT_S8_UINT)
      shape = {1, 1, 4};

    stage.Create(m_pDriver, m_Device, pattern.size(), 1, 0);

    void *ptr = stage.Map();
    if(!ptr)
      return;
    memcpy(ptr, pattern.data(), pattern.size());
    stage.Unmap();

    VkBufferCreateInfo bufInfo = {
        VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0,
        pattern.size() * (PatternBatchWidth / DiscardPatternWidth) *
            (PatternBatchHeight / DiscardPatternHeight),
        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
    };

    vkr = m_pDriver->vkCreateBuffer(dev, &bufInfo, NULL, &buf);
    CheckVkResult(vkr);

    MemoryAllocation alloc = m_pDriver->AllocateMemoryForResource(
        buf, MemoryScope::ImmutableReplayDebug, MemoryType::GPULocal);

    if(alloc.mem == VK_NULL_HANDLE)
      return;

    vkr = vt->BindBufferMemory(Unwrap(dev), Unwrap(buf), Unwrap(alloc.mem), alloc.offs);
    CheckVkResult(vkr);

    rdcarray<VkBufferCopy> bufRegions;
    VkBufferCopy bufCopy;
    // copy one row at a time (row of blocks, for blocks)
    bufCopy.size = shape.bytes * DiscardPatternWidth / shape.width;

    const uint32_t numHorizBatches = PatternBatchWidth / DiscardPatternWidth;

    for(uint32_t y = 0; y < PatternBatchHeight / shape.height; y++)
    {
      // copy from the rows sequentially
      bufCopy.srcOffset = bufCopy.size * (y % (DiscardPatternHeight / shape.height));
      for(uint32_t x = 0; x < numHorizBatches; x++)
      {
        bufCopy.dstOffset = y * bufCopy.size * numHorizBatches + x * bufCopy.size;
        bufRegions.push_back(bufCopy);
      }
    }

    // copy byte-packed second stencil pattern afterwards
    if(aspectFlags & VK_IMAGE_ASPECT_STENCIL_BIT)
    {
      const VkDeviceSize srcDepthOffset = DiscardPatternWidth * DiscardPatternHeight * shape.bytes;
      const VkDeviceSize dstDepthOffset = PatternBatchWidth * PatternBatchHeight * shape.bytes;

      bufCopy.size = DiscardPatternWidth;
      for(uint32_t y = 0; y < PatternBatchHeight; y++)
      {
        // copy from the rows sequentially. The source data is depth-pitched
        bufCopy.srcOffset = srcDepthOffset + bufCopy.size * shape.bytes * (y % DiscardPatternHeight);
        for(uint32_t x = 0; x < numHorizBatches; x++)
        {
          bufCopy.dstOffset = dstDepthOffset + y * bufCopy.size * numHorizBatches + x * bufCopy.size;
          bufRegions.push_back(bufCopy);
        }
      }
    }

    vt->CmdCopyBuffer(Unwrap(cmd), Unwrap(stage.buf), Unwrap(buf), (uint32_t)bufRegions.size(),
                      bufRegions.data());

    m_DiscardPatterns[key] = buf;

    VkBufferMemoryBarrier bufBarrier = {
        VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
        NULL,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        VK_ACCESS_TRANSFER_READ_BIT,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        Unwrap(buf),
        0,
        VK_WHOLE_SIZE,
    };

    DoPipelineBarrier(cmd, 1, &bufBarrier);
  }

  rdcarray<VkBufferImageCopy> mainCopies, stencilCopies;

  VkExtent3D extent;

  // copy each slice/mip individually
  for(uint32_t a = 0; a < imInfo.arrayLayers; a++)
  {
    if(a < discardRange.baseArrayLayer || a >= discardRange.baseArrayLayer + discardRange.layerCount)
      continue;

    extent = imInfo.extent;
    extent.width = RDCMIN(extent.width, discardRect.offset.x + discardRect.extent.width);
    extent.height = RDCMIN(extent.height, discardRect.offset.y + discardRect.extent.height);

    for(uint32_t m = 0; m < imInfo.mipLevels; m++)
    {
      if(m >= discardRange.baseMipLevel && m < discardRange.baseMipLevel + discardRange.levelCount)
      {
        for(uint32_t z = 0; z < extent.depth; z++)
        {
          for(uint32_t y = discardRect.offset.y; y < extent.height; y += PatternBatchHeight)
          {
            for(uint32_t x = discardRect.offset.x; x < extent.width; x += PatternBatchWidth)
            {
              VkBufferImageCopy region = {
                  0,
                  0,
                  0,
                  {aspectFlags, m, a, 1},
                  {
                      (int)x, (int)y, (int)z,
                  },
              };

              region.imageExtent.width = RDCMIN(PatternBatchWidth, extent.width - x);
              region.imageExtent.height = RDCMIN(PatternBatchHeight, extent.height - y);
              region.imageExtent.depth = 1;

              region.bufferRowLength = PatternBatchWidth;

              // for depth/stencil copies, write depth first
              if(aspectFlags == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT))
                region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;

              if(aspectFlags != VK_IMAGE_ASPECT_STENCIL_BIT)
                mainCopies.push_back(region);

              if(aspectFlags & VK_IMAGE_ASPECT_STENCIL_BIT)
              {
                uint32_t depthStride = (imInfo.format == VK_FORMAT_D16_UNORM_S8_UINT ? 2 : 4);
                VkDeviceSize depthOffset = PatternBatchWidth * PatternBatchHeight * depthStride;

                // if it's a depth/stencil format, write stencil separately
                region.bufferOffset = depthOffset;
                region.bufferRowLength = PatternBatchWidth;
                region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;

                stencilCopies.push_back(region);
              }
            }
          }
        }
      }

      // update the extent for the next mip
      extent.width = RDCMAX(extent.width >> 1, 1U);
      extent.height = RDCMAX(extent.height >> 1, 1U);
      extent.depth = RDCMAX(extent.depth >> 1, 1U);
    }
  }

  VkImageMemoryBarrier dstimBarrier = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
      NULL,
      VK_ACCESS_ALL_READ_BITS | VK_ACCESS_ALL_WRITE_BITS,
      VK_ACCESS_TRANSFER_WRITE_BIT,
      curLayout,
      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
      VK_QUEUE_FAMILY_IGNORED,
      VK_QUEUE_FAMILY_IGNORED,
      Unwrap(image),
      barrierDiscardRange,
  };

  DoPipelineBarrier(cmd, 1, &dstimBarrier);

  if(!mainCopies.empty())
    ObjDisp(cmd)->CmdCopyBufferToImage(Unwrap(cmd), Unwrap(buf), Unwrap(image),
                                       VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                       (uint32_t)mainCopies.size(), mainCopies.data());

  if(!stencilCopies.empty())
    ObjDisp(cmd)->CmdCopyBufferToImage(Unwrap(cmd), Unwrap(buf), Unwrap(image),
                                       VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                       (uint32_t)stencilCopies.size(), stencilCopies.data());

  dstimBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
  dstimBarrier.newLayout = curLayout;
  dstimBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  dstimBarrier.dstAccessMask = VK_ACCESS_ALL_WRITE_BITS | VK_ACCESS_ALL_READ_BITS;

  DoPipelineBarrier(cmd, 1, &dstimBarrier);
}

void VulkanDebugManager::InitReadbackBuffer(VkDeviceSize sz)
{
  if(m_ReadbackWindow.buf == VK_NULL_HANDLE || m_ReadbackWindow.sz < sz)
  {
    if(m_ReadbackWindow.buf != VK_NULL_HANDLE)
    {
      m_ReadbackWindow.Destroy();
    }

    VkDevice dev = m_pDriver->GetDev();
    m_ReadbackWindow.Create(m_pDriver, dev, AlignUp(sz, (VkDeviceSize)4096), 1,
                            GPUBuffer::eGPUBufferReadback);

    RDCLOG("Allocating readback window of %llu bytes", m_ReadbackWindow.sz);

    VkResult vkr = ObjDisp(dev)->MapMemory(Unwrap(dev), Unwrap(m_ReadbackWindow.mem), 0,
                                           VK_WHOLE_SIZE, 0, (void **)&m_ReadbackPtr);
    CheckVkResult(vkr);
    if(!m_ReadbackPtr)
    {
      RDCERR("Manually reporting failed memory map");
      CheckVkResult(VK_ERROR_MEMORY_MAP_FAILED);
    }
  }
}

void VulkanReplay::PatchReservedDescriptors(const VulkanStatePipeline &pipe,
                                            VkDescriptorPool &descpool,
                                            rdcarray<VkDescriptorSetLayout> &setLayouts,
                                            rdcarray<VkDescriptorSet> &descSets,
                                            VkShaderStageFlagBits patchedBindingStage,
                                            const VkDescriptorSetLayoutBinding *newBindings,
                                            size_t newBindingsCount)
{
  VkDevice dev = m_Device;
  VulkanCreationInfo &creationInfo = m_pDriver->m_CreationInfo;

  const VulkanCreationInfo::Pipeline &pipeInfo = creationInfo.m_Pipeline[pipe.pipeline];

  VkResult vkr = VK_SUCCESS;

  rdcarray<VkWriteDescriptorSet> descWrites;

  struct AllocedWrites
  {
    ~AllocedWrites()
    {
      for(VkDescriptorImageInfo *a : imgWrites)
        delete[] a;
      for(VkDescriptorBufferInfo *a : bufWrites)
        delete[] a;
      for(VkBufferView *a : bufViewWrites)
        delete[] a;
      for(VkWriteDescriptorSetInlineUniformBlock *a : inlineWrites)
        delete a;
    }

    rdcarray<VkDescriptorImageInfo *> imgWrites;
    rdcarray<VkDescriptorBufferInfo *> bufWrites;
    rdcarray<VkBufferView *> bufViewWrites;
    rdcarray<VkWriteDescriptorSetInlineUniformBlock *> inlineWrites;
  } alloced;

  rdcarray<VkDescriptorImageInfo *> &allocImgWrites = alloced.imgWrites;
  rdcarray<VkDescriptorBufferInfo *> &allocBufWrites = alloced.bufWrites;
  rdcarray<VkBufferView *> &allocBufViewWrites = alloced.bufViewWrites;

  rdcarray<VkWriteDescriptorSetInlineUniformBlock *> &allocInlineWrites = alloced.inlineWrites;

  // one for each descriptor type. 1 of each to start with, we then increment for each descriptor
  // we need to allocate
  VkDescriptorPoolSize poolSizes[12] = {
      {VK_DESCRIPTOR_TYPE_SAMPLER, 1},
      {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1},
      {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1},
      {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1},
      {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1},
      {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1},
      {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1},
      {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1},
      {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1},
      {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1},
      {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1},
      {VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK, 0},
  };

  VkDescriptorPoolInlineUniformBlockCreateInfo inlineCreateInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_INLINE_UNIFORM_BLOCK_CREATE_INFO,
  };

  static const uint32_t InlinePoolIndex = 11;

  uint32_t poolSizeCount = InlinePoolIndex;

  // count up our own
  for(size_t i = 0; i < newBindingsCount; i++)
  {
    RDCASSERT(newBindings[i].descriptorType < ARRAY_COUNT(poolSizes), newBindings[i].descriptorType);
    poolSizes[newBindings[i].descriptorType].descriptorCount += newBindings[i].descriptorCount;
  }

  // need to add our added bindings to the first descriptor set
  rdcarray<VkDescriptorSetLayoutBinding> bindings(newBindings, newBindingsCount);

  // if there are fewer sets bound than were declared in the pipeline layout, only process the
  // bound sets (as otherwise we'd fail to copy from them). Assume the application knew what it
  // was doing and the other sets are statically unused.
  setLayouts.resize(RDCMIN(pipe.descSets.size(), pipeInfo.descSetLayouts.size()));

  size_t boundDescs = setLayouts.size();

  // need at least one set, if the shader isn't using any we'll just make our own
  if(setLayouts.empty())
    setLayouts.resize(1);

  // start with the limits as they are, and subtract off them incrementally. When any limit would
  // drop below 0, we fail.
  uint32_t maxPerStageDescriptorSamplers[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSamplers,
  };
  uint32_t maxPerStageDescriptorUniformBuffers[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorUniformBuffers,
  };
  uint32_t maxPerStageDescriptorStorageBuffers[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageBuffers,
  };
  uint32_t maxPerStageDescriptorSampledImages[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorSampledImages,
  };
  uint32_t maxPerStageDescriptorStorageImages[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorStorageImages,
  };
  uint32_t maxPerStageDescriptorInputAttachments[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
      m_pDriver->GetDeviceProps().limits.maxPerStageDescriptorInputAttachments,
  };
  uint32_t maxPerStageResources[6] = {
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
      m_pDriver->GetDeviceProps().limits.maxPerStageResources,
  };
  uint32_t maxDescriptorSetSamplers = m_pDriver->GetDeviceProps().limits.maxDescriptorSetSamplers;
  uint32_t maxDescriptorSetUniformBuffers =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetUniformBuffers;
  uint32_t maxDescriptorSetUniformBuffersDynamic =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetUniformBuffersDynamic;
  uint32_t maxDescriptorSetStorageBuffers =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetStorageBuffers;
  uint32_t maxDescriptorSetStorageBuffersDynamic =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetStorageBuffersDynamic;
  uint32_t maxDescriptorSetSampledImages =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetSampledImages;
  uint32_t maxDescriptorSetStorageImages =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetStorageImages;
  uint32_t maxDescriptorSetInputAttachments =
      m_pDriver->GetDeviceProps().limits.maxDescriptorSetInputAttachments;

  uint32_t maxDescriptorSetInlineUniformBlocks = 0;
  uint32_t maxPerStageDescriptorInlineUniformBlocks[6] = {};

  if(m_pDriver->GetExtensions(NULL).ext_EXT_inline_uniform_block)
  {
    VkPhysicalDeviceInlineUniformBlockProperties inlineProps = {
        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_PROPERTIES,
    };

    VkPhysicalDeviceProperties2 availBase = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
    availBase.pNext = &inlineProps;
    m_pDriver->vkGetPhysicalDeviceProperties2(m_pDriver->GetPhysDev(), &availBase);

    maxDescriptorSetInlineUniformBlocks = inlineProps.maxDescriptorSetInlineUniformBlocks;
    for(size_t i = 0; i < ARRAY_COUNT(maxPerStageDescriptorInlineUniformBlocks); i++)
      maxPerStageDescriptorInlineUniformBlocks[i] =
          inlineProps.maxPerStageDescriptorInlineUniformBlocks;
  }

  bool error = false;

#define UPDATE_AND_CHECK_LIMIT(maxLimit)                                                   \
  if(!error)                                                                               \
  {                                                                                        \
    if(descriptorCount > maxLimit)                                                         \
    {                                                                                      \
      error = true;                                                                        \
      RDCWARN("Limit %s is exceeded. Cannot patch in required descriptor(s).", #maxLimit); \
    }                                                                                      \
    else                                                                                   \
    {                                                                                      \
      maxLimit -= descriptorCount;                                                         \
    }                                                                                      \
  }

#define UPDATE_AND_CHECK_STAGE_LIMIT(maxLimit)                                                 \
  if(!error)                                                                                   \
  {                                                                                            \
    for(uint32_t sbit = 0; sbit < 6; sbit++)                                                   \
    {                                                                                          \
      if(newBind.stageFlags & (1U << sbit))                                                    \
      {                                                                                        \
        if(descriptorCount > maxLimit[sbit])                                                   \
        {                                                                                      \
          error = true;                                                                        \
          RDCWARN("Limit %s is exceeded. Cannot patch in required descriptor(s).", #maxLimit); \
        }                                                                                      \
        else                                                                                   \
        {                                                                                      \
          maxLimit[sbit] -= descriptorCount;                                                   \
        }                                                                                      \
      }                                                                                        \
    }                                                                                          \
  }

  for(size_t i = 0; !error && i < setLayouts.size(); i++)
  {
    bool hasImmutableSamplers = false;

    // except for the first layout we need to start from scratch
    if(i > 0)
      bindings.clear();

    // if the shader had no descriptor sets at all, i will be invalid, so just skip and add a set
    // with only our own bindings.
    if(i < pipeInfo.descSetLayouts.size() && i < pipe.descSets.size() &&
       pipe.descSets[i].pipeLayout != ResourceId())
    {
      const VulkanCreationInfo::PipelineLayout &pipelineLayoutInfo =
          creationInfo.m_PipelineLayout[pipe.descSets[i].pipeLayout];

      if(pipelineLayoutInfo.descSetLayouts[i] == ResourceId())
        continue;

      // use the descriptor set layout from when it was bound. If the pipeline layout declared a
      // descriptor set layout for this set, but it's statically unused, it may be complete
      // garbage and doesn't match what the shader uses. However the pipeline layout at descriptor
      // set bind time must have been compatible and valid so we can use it. If this set *is* used
      // then the pipeline layout at bind time must be compatible with the pipeline's pipeline
      // layout, so we're fine too.
      const DescSetLayout &origLayout =
          creationInfo.m_DescSetLayout[pipelineLayoutInfo.descSetLayouts[i]];

      WrappedVulkan::DescriptorSetInfo &setInfo =
          m_pDriver->m_DescriptorSetState[pipe.descSets[i].descSet];

      for(size_t b = 0; !error && b < origLayout.bindings.size(); b++)
      {
        const DescSetLayout::Binding &bind = origLayout.bindings[b];

        // skip empty bindings
        if(bind.descriptorType == VK_DESCRIPTOR_TYPE_MAX_ENUM)
          continue;

        uint32_t descriptorCount = bind.descriptorCount;

        if(bind.variableSize)
          descriptorCount = setInfo.data.variableDescriptorCount;

        // make room in the pool
        if(bind.descriptorType == VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK)
        {
          poolSizes[InlinePoolIndex].descriptorCount += descriptorCount;
          inlineCreateInfo.maxInlineUniformBlockBindings++;
        }
        else
        {
          poolSizes[bind.descriptorType].descriptorCount += descriptorCount;
        }

        VkDescriptorSetLayoutBinding newBind;
        // offset the binding. We offset all sets to make it easier for patching - don't need to
        // conditionally patch shader bindings depending on which set they're in.
        newBind.binding = uint32_t(b + newBindingsCount);
        newBind.descriptorCount = descriptorCount;
        newBind.descriptorType = bind.descriptorType;

        // we only need it available for compute, just make all bindings visible otherwise dynamic
        // buffer offsets could be indexed wrongly. Consider the case where we have binding 0 as a
        // fragment UBO, and binding 1 as a vertex UBO. Then there are two dynamic offsets, and
        // the second is the one we want to use with ours. If we only add the compute visibility
        // bit to the second UBO, then suddenly it's the *first* offset that we must provide.
        // Instead of trying to remap offsets to match, we simply make every binding compute
        // visible so the ordering is still the same. Since compute and graphics are disjoint this
        // is safe.
        if(patchedBindingStage != 0)
          newBind.stageFlags = patchedBindingStage;
        else
          newBind.stageFlags = bind.stageFlags;

        switch(bind.descriptorType)
        {
          case VK_DESCRIPTOR_TYPE_SAMPLER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetSamplers);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorSamplers);
            break;
          case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetSampledImages);
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetSamplers);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorSamplers);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorSampledImages);
            break;
          case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetSampledImages);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorSampledImages);
            break;
          case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetStorageImages);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorStorageImages);
            break;
          case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetSampledImages);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorSampledImages);
            break;
          case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetStorageImages);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorStorageImages);
            break;
          case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetUniformBuffers);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorUniformBuffers);
            break;
          case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetStorageBuffers);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorStorageBuffers);
            break;
          case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetUniformBuffersDynamic);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorUniformBuffers);
            break;
          case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetStorageBuffersDynamic);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorStorageBuffers);
            break;
          case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT:
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetInputAttachments);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorInputAttachments);
            break;
          case VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK:
            descriptorCount = 1;
            UPDATE_AND_CHECK_LIMIT(maxDescriptorSetInlineUniformBlocks);
            UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageDescriptorInlineUniformBlocks);
            break;
          default: break;
        }

        UPDATE_AND_CHECK_STAGE_LIMIT(maxPerStageResources);

        if(bind.immutableSampler)
        {
          hasImmutableSamplers = true;
          VkSampler *samplers = new VkSampler[bind.descriptorCount];
          newBind.pImmutableSamplers = samplers;
          for(uint32_t s = 0; s < bind.descriptorCount; s++)
            samplers[s] = GetResourceManager()->GetCurrentHandle<VkSampler>(bind.immutableSampler[s]);
        }
        else
        {
          newBind.pImmutableSamplers = NULL;
        }

        bindings.push_back(newBind);
      }
    }

    VkDescriptorSetLayoutCreateInfo descsetLayoutInfo = {
        VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
        NULL,
        0,
        (uint32_t)bindings.size(),
        bindings.data(),
    };

    if(!error)
    {
      // create new offseted descriptor layout
      vkr = m_pDriver->vkCreateDescriptorSetLayout(dev, &descsetLayoutInfo, NULL, &setLayouts[i]);
      CheckVkResult(vkr);
    }

    if(hasImmutableSamplers)
    {
      for(const VkDescriptorSetLayoutBinding &bind : bindings)
        delete[] bind.pImmutableSamplers;
    }
  }

  // if we hit an error, we can't create the descriptor set so bail out now
  if(error)
    return;

  VkDescriptorPoolCreateInfo poolCreateInfo = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
  // 1 set for each layout
  poolCreateInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
  poolCreateInfo.maxSets = (uint32_t)setLayouts.size();
  poolCreateInfo.poolSizeCount = poolSizeCount;
  poolCreateInfo.pPoolSizes = poolSizes;

  if(inlineCreateInfo.maxInlineUniformBlockBindings > 0)
  {
    poolCreateInfo.poolSizeCount++;
    poolCreateInfo.pNext = &inlineCreateInfo;
  }

  // create descriptor pool with enough space for our descriptors
  vkr = m_pDriver->vkCreateDescriptorPool(dev, &poolCreateInfo, NULL, &descpool);
  CheckVkResult(vkr);

  // allocate all the descriptors
  VkDescriptorSetAllocateInfo descSetAllocInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
      NULL,
      descpool,
      (uint32_t)setLayouts.size(),
      setLayouts.data(),
  };

  descSets.resize(setLayouts.size());
  m_pDriver->vkAllocateDescriptorSets(dev, &descSetAllocInfo, descSets.data());

  // copy the data across from the real descriptors into our adjusted bindings
  for(size_t i = 0; i < boundDescs; i++)
  {
    if(pipe.descSets[i].descSet == ResourceId())
      continue;

    const VulkanCreationInfo::PipelineLayout &pipelineLayoutInfo =
        creationInfo.m_PipelineLayout[pipe.descSets[i].pipeLayout];

    if(pipelineLayoutInfo.descSetLayouts[i] == ResourceId())
      continue;

    // as above we use the pipeline layout that was originally used to bind this descriptor set
    // and not the pipeline layout from the pipeline, in case the pipeline statically doesn't use
    // this set and so its descriptor set layout is garbage (doesn't match the actual bound
    // descriptor set)
    const DescSetLayout &origLayout =
        creationInfo.m_DescSetLayout[pipelineLayoutInfo.descSetLayouts[i]];

    WrappedVulkan::DescriptorSetInfo &setInfo =
        m_pDriver->m_DescriptorSetState[pipe.descSets[i].descSet];

    {
      // push descriptors don't have a source to copy from, we need to add writes
      VkWriteDescriptorSet write = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
      write.dstSet = descSets[i];

      // Only write bindings that actually exist in the current descriptor
      // set. If there are bindings that aren't set, assume the app knows
      // what it's doing and the remaining bindings are unused.
      for(size_t b = 0; b < setInfo.data.binds.size(); b++)
      {
        const DescSetLayout::Binding &bind = origLayout.bindings[b];

        // skip empty bindings
        if(bind.descriptorType == VK_DESCRIPTOR_TYPE_MAX_ENUM)
          continue;

        uint32_t descriptorCount = bind.descriptorCount;

        if(bind.variableSize)
          descriptorCount = setInfo.data.variableDescriptorCount;

        DescriptorSetSlot *slot = setInfo.data.binds[b];

        write.dstBinding = uint32_t(b + newBindingsCount);
        write.dstArrayElement = 0;
        write.descriptorCount = descriptorCount;
        write.descriptorType = bind.descriptorType;

        switch(write.descriptorType)
        {
          case VK_DESCRIPTOR_TYPE_SAMPLER:
          case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
          case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
          case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
          case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT:
          {
            VkDescriptorImageInfo *out = new VkDescriptorImageInfo[write.descriptorCount];
            for(uint32_t w = 0; w < write.descriptorCount; w++)
            {
              const DescriptorSetSlotImageInfo &src = slot[w].imageInfo;

              out[w].imageLayout = src.imageLayout;
              out[w].sampler = GetResourceManager()->GetCurrentHandle<VkSampler>(src.sampler);
              out[w].imageView = GetResourceManager()->GetCurrentHandle<VkImageView>(src.imageView);
            }

            write.pImageInfo = out;
            allocImgWrites.push_back(out);
            break;
          }
          case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
          case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
          {
            VkBufferView *out = new VkBufferView[write.descriptorCount];
            for(uint32_t w = 0; w < write.descriptorCount; w++)
              out[w] = GetResourceManager()->GetCurrentHandle<VkBufferView>(slot[w].texelBufferView);
            write.pTexelBufferView = out;
            allocBufViewWrites.push_back(out);
            break;
          }
          case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
          case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
          case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
          case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC:
          {
            VkDescriptorBufferInfo *out = new VkDescriptorBufferInfo[write.descriptorCount];
            for(uint32_t w = 0; w < write.descriptorCount; w++)
            {
              const DescriptorSetSlotBufferInfo &src = slot[w].bufferInfo;

              out[w].offset = src.offset;
              out[w].range = src.range;
              out[w].buffer = GetResourceManager()->GetCurrentHandle<VkBuffer>(src.buffer);
            }
            write.pBufferInfo = out;
            allocBufWrites.push_back(out);
            break;
          }
          case VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK:
          {
            allocInlineWrites.push_back(new VkWriteDescriptorSetInlineUniformBlock);
            VkWriteDescriptorSetInlineUniformBlock *inlineWrite = allocInlineWrites.back();
            inlineWrite->sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_INLINE_UNIFORM_BLOCK;
            inlineWrite->pNext = NULL;
            inlineWrite->dataSize = descriptorCount;
            inlineWrite->pData = setInfo.data.inlineBytes.data() + slot[0].inlineOffset;
            write.pNext = inlineWrite;
            break;
          }
          default: RDCERR("Unexpected descriptor type %d", write.descriptorType);
        }

        // skip validity check for inline uniform block as the descriptor count means something
        // different
        if(write.descriptorType == VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK)
        {
          write.descriptorCount = descriptorCount;
          descWrites.push_back(write);
          continue;
        }

        // start with no descriptors
        write.descriptorCount = 0;

        for(uint32_t w = 0; w < descriptorCount; w++)
        {
          // if this write is valid, we increment the descriptor count and continue
          if(IsValid(m_pDriver->NULLDescriptorsAllowed(), write, w - write.dstArrayElement))
          {
            write.descriptorCount++;
          }
          else
          {
            // if this write isn't valid, then we first check to see if we had any previous
            // pending writes in the array we were going to batch together, if so we add them.
            if(write.descriptorCount > 0)
              descWrites.push_back(write);

            // skip past any previous descriptors we just wrote, as well as the current invalid
            // one
            if(write.pBufferInfo)
              write.pBufferInfo += write.descriptorCount + 1;
            if(write.pImageInfo)
              write.pImageInfo += write.descriptorCount + 1;
            if(write.pTexelBufferView)
              write.pTexelBufferView += write.descriptorCount + 1;

            // now start again from 0 descriptors, at the next array element
            write.dstArrayElement += write.descriptorCount + 1;
            write.descriptorCount = 0;
          }
        }

        // if there are any left, add them here
        if(write.descriptorCount > 0)
          descWrites.push_back(write);

        // don't leak the arrays and cause double deletes, NULL them after each time
        write.pImageInfo = NULL;
        write.pBufferInfo = NULL;
        write.pTexelBufferView = NULL;
      }
    }
  }

  m_pDriver->vkUpdateDescriptorSets(dev, (uint32_t)descWrites.size(), descWrites.data(), 0, NULL);
}

void VulkanDebugManager::CustomShaderRendering::Destroy(WrappedVulkan *driver)
{
  driver->vkDestroyRenderPass(driver->GetDev(), TexRP, NULL);
  driver->vkDestroyFramebuffer(driver->GetDev(), TexFB, NULL);
  driver->vkDestroyImage(driver->GetDev(), TexImg, NULL);
  for(size_t i = 0; i < ARRAY_COUNT(TexImgView); i++)
    driver->vkDestroyImageView(driver->GetDev(), TexImgView[i], NULL);
  driver->vkFreeMemory(driver->GetDev(), TexMem, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), TexPipeline, NULL);
}

void VulkanReplay::CreateResources()
{
  m_Device = m_pDriver->GetDev();

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.0f);

  m_General.Init(m_pDriver, VK_NULL_HANDLE);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.1f);

  m_TexRender.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.3f);

  m_Overlay.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.4f);

  m_MeshRender.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.6f);

  m_VertexPick.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.7f);

  m_PixelPick.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.75f);

  m_PixelHistory.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.8f);

  m_Histogram.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 0.9f);

  m_ShaderDebugData.Init(m_pDriver, m_General.DescriptorPool);

  RenderDoc::Inst().SetProgress(LoadProgress::DebugManagerInit, 1.0f);

  GpaVkContextOpenInfo context = {Unwrap(m_pDriver->GetInstance()), Unwrap(m_pDriver->GetPhysDev()),
                                  Unwrap(m_pDriver->GetDev())};

  if(!m_pDriver->GetReplay()->IsRemoteProxy() && Vulkan_HardwareCounters())
  {
    AMDCounters *counters = NULL;

    GPUVendor vendor = m_pDriver->GetDriverInfo().Vendor();

    if(vendor == GPUVendor::AMD || vendor == GPUVendor::Samsung)
    {
      RDCLOG("AMD GPU detected - trying to initialise AMD counters");
      counters = new AMDCounters();
    }
    else
    {
      RDCLOG("%s GPU detected - no counters available", ToStr(vendor).c_str());
    }

    if(counters && counters->Init(AMDCounters::ApiType::Vk, (void *)&context))
    {
      m_pAMDCounters = counters;
    }
    else
    {
      delete counters;
      m_pAMDCounters = NULL;
    }
  }
}

void VulkanReplay::DestroyResources()
{
  ClearPostVSCache();
  ClearFeedbackCache();

  m_General.Destroy(m_pDriver);
  m_TexRender.Destroy(m_pDriver);
  m_Overlay.Destroy(m_pDriver);
  m_VertexPick.Destroy(m_pDriver);
  m_PixelPick.Destroy(m_pDriver);
  m_PixelHistory.Destroy(m_pDriver);
  m_Histogram.Destroy(m_pDriver);
  m_PostVS.Destroy(m_pDriver);

  SAFE_DELETE(m_pAMDCounters);
}

void VulkanReplay::GeneralMisc::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VkResult vkr = VK_SUCCESS;

  VkDescriptorPoolSize descPoolTypes[] = {
      {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 320},
      {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 128},
      {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 32},
      {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 128},
      {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 128},
      {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 64},
      {VK_DESCRIPTOR_TYPE_SAMPLER, 64},
      {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 32},
  };

  VkDescriptorPoolCreateInfo descPoolInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
      NULL,
      0,
      32,
      ARRAY_COUNT(descPoolTypes),
      &descPoolTypes[0],
  };

  // create descriptor pool
  vkr = driver->vkCreateDescriptorPool(driver->GetDev(), &descPoolInfo, NULL, &DescriptorPool);
  driver->CheckVkResult(vkr);

  CREATE_OBJECT(PointSampler, VK_FILTER_NEAREST);
}

void VulkanReplay::GeneralMisc::Destroy(WrappedVulkan *driver)
{
  if(DescriptorPool == VK_NULL_HANDLE)
    return;

  driver->vkDestroyDescriptorPool(driver->GetDev(), DescriptorPool, NULL);
  driver->vkDestroySampler(driver->GetDev(), PointSampler, NULL);
}

void VulkanReplay::TextureRendering::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VkResult vkr = VK_SUCCESS;

  VulkanShaderCache *shaderCache = driver->GetShaderCache();

  CREATE_OBJECT(PointSampler, VK_FILTER_NEAREST);
  CREATE_OBJECT(LinearSampler, VK_FILTER_LINEAR);

  CREATE_OBJECT(DescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL},
                    {1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL},
                    {6, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {7, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {8, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {9, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {10, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2, VK_SHADER_STAGE_ALL, NULL},
                    {11, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {12, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {13, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {14, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {15, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {16, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {17, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {18, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {19, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {20, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {50, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_ALL, &PointSampler},
                    {51, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_ALL, &LinearSampler},
                });

  CREATE_OBJECT(PipeLayout, DescSetLayout, 0);

  for(size_t i = 0; i < ARRAY_COUNT(DescSet); i++)
  {
    CREATE_OBJECT(DescSet[i], descriptorPool, DescSetLayout);
  }

  UBO.Create(driver, driver->GetDev(), 128, 10, 0);
  RDCCOMPILE_ASSERT(sizeof(TexDisplayUBOData) <= 128, "tex display size");

  HeatmapUBO.Create(driver, driver->GetDev(), 512, 10, 0);
  RDCCOMPILE_ASSERT(sizeof(HeatmapData) <= 512, "tex display size");

  {
    VkRenderPass SRGBA8RP = VK_NULL_HANDLE;
    VkRenderPass RGBA16RP = VK_NULL_HANDLE;
    VkRenderPass RGBA32RP = VK_NULL_HANDLE;

    CREATE_OBJECT(SRGBA8RP, VK_FORMAT_R8G8B8A8_SRGB);
    CREATE_OBJECT(RGBA16RP, VK_FORMAT_R16G16B16A16_SFLOAT);
    CREATE_OBJECT(RGBA32RP, VK_FORMAT_R32G32B32A32_SFLOAT);

    ConciseGraphicsPipeline texDisplayInfo = {
        SRGBA8RP,
        PipeLayout,
        shaderCache->GetBuiltinModule(BuiltinShader::BlitVS),
        shaderCache->GetBuiltinModule(BuiltinShader::TexDisplayFS),
        {VK_DYNAMIC_STATE_VIEWPORT},
        VK_SAMPLE_COUNT_1_BIT,
        false,    // sampleRateShading
        false,    // depthEnable
        false,    // stencilEnable
        VK_STENCIL_OP_KEEP,
        true,     // colourOutput
        false,    // blendEnable
        VK_BLEND_FACTOR_ONE,
        VK_BLEND_FACTOR_ZERO,
        0xf,    // writeMask
    };

    ConciseGraphicsPipeline texRemapInfo = texDisplayInfo;

    CREATE_OBJECT(Pipeline, texDisplayInfo);

    texDisplayInfo.renderPass = RGBA32RP;
    CREATE_OBJECT(F32Pipeline, texDisplayInfo);

    texDisplayInfo.renderPass = RGBA16RP;
    CREATE_OBJECT(F16Pipeline, texDisplayInfo);

    texDisplayInfo.renderPass = SRGBA8RP;
    texDisplayInfo.blendEnable = true;
    texDisplayInfo.srcBlend = VK_BLEND_FACTOR_SRC_ALPHA;
    texDisplayInfo.dstBlend = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
    CREATE_OBJECT(BlendPipeline, texDisplayInfo);

    VkFormat formats[3] = {VK_FORMAT_R8G8B8A8_UINT, VK_FORMAT_R16G16B16A16_UINT,
                           VK_FORMAT_R32G32B32A32_UINT};
    CompType cast[3] = {CompType::Float, CompType::UInt, CompType::SInt};

    for(int f = 0; f < 3; f++)
    {
      for(int i = 0; i < 3; i++)
      {
        texRemapInfo.fragment =
            shaderCache->GetBuiltinModule(BuiltinShader::TexRemap, BuiltinShaderBaseType(i));

        CREATE_OBJECT(texRemapInfo.renderPass, GetViewCastedFormat(formats[f], cast[i]));

        CREATE_OBJECT(RemapPipeline[f][i][0], texRemapInfo);

        driver->vkDestroyRenderPass(driver->GetDev(), texRemapInfo.renderPass, NULL);

        // reuse float 'green' as srgb
        if(f == 0 && i == 0)
        {
          CREATE_OBJECT(texRemapInfo.renderPass, VK_FORMAT_R8G8B8A8_SRGB);

          CREATE_OBJECT(RemapPipeline[f][i][1], texRemapInfo);

          driver->vkDestroyRenderPass(driver->GetDev(), texRemapInfo.renderPass, NULL);
        }
      }
    }

    // make versions that only write to green, for doing two-pass stencil writes
    texRemapInfo.writeMask = texDisplayInfo.writeMask = 0x2;

    for(int f = 0; f < 3; f++)
    {
      // only create this for uint, it's normally only needed there
      int i = 1;

      texRemapInfo.fragment =
          shaderCache->GetBuiltinModule(BuiltinShader::TexRemap, BuiltinShaderBaseType(i));

      CREATE_OBJECT(texRemapInfo.renderPass, GetViewCastedFormat(formats[f], cast[i]));

      CREATE_OBJECT(RemapPipeline[f][i][1], texRemapInfo);

      driver->vkDestroyRenderPass(driver->GetDev(), texRemapInfo.renderPass, NULL);
    }

    texDisplayInfo.renderPass = SRGBA8RP;
    CREATE_OBJECT(PipelineGreenOnly, texDisplayInfo);

    texDisplayInfo.renderPass = RGBA32RP;
    CREATE_OBJECT(F32PipelineGreenOnly, texDisplayInfo);

    texDisplayInfo.renderPass = RGBA16RP;
    CREATE_OBJECT(F16PipelineGreenOnly, texDisplayInfo);

    driver->vkDestroyRenderPass(driver->GetDev(), SRGBA8RP, NULL);
    driver->vkDestroyRenderPass(driver->GetDev(), RGBA16RP, NULL);
    driver->vkDestroyRenderPass(driver->GetDev(), RGBA32RP, NULL);
  }

  // create dummy images for filling out the texdisplay descriptors
  // in slots that are skipped by dynamic branching (e.g. 3D texture
  // when we're displaying a 2D, etc).
  {
    VkCommandBuffer cmd = driver->GetNextCmd();

    if(cmd == VK_NULL_HANDLE)
      return;

    VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                          VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

    vkr = ObjDisp(cmd)->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
    driver->CheckVkResult(vkr);

    int index = 0;

    // we pick RGBA8 formats to be guaranteed they will be supported
    VkFormat formats[] = {VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UINT,
                          VK_FORMAT_R8G8B8A8_SINT, VK_FORMAT_D16_UNORM};
    VkImageType types[] = {VK_IMAGE_TYPE_1D, VK_IMAGE_TYPE_2D, VK_IMAGE_TYPE_3D, VK_IMAGE_TYPE_2D};
    VkImageViewType viewtypes[] = {
        VK_IMAGE_VIEW_TYPE_1D_ARRAY, VK_IMAGE_VIEW_TYPE_2D_ARRAY, VK_IMAGE_VIEW_TYPE_3D,
        VK_IMAGE_VIEW_TYPE_2D_ARRAY,
        driver->GetDeviceEnabledFeatures().imageCubeArray ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
                                                          : VK_IMAGE_VIEW_TYPE_CUBE,
    };
    VkSampleCountFlagBits sampleCounts[] = {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_1_BIT,
                                            VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_4_BIT};

    // type max is one higher than the last RESTYPE, and RESTYPES are 1-indexed
    RDCCOMPILE_ASSERT(RESTYPE_TEXTYPEMAX - 1 == ARRAY_COUNT(types),
                      "RESTYPE values don't match formats for dummy images");

    RDCCOMPILE_ASSERT(sizeof(DummyImages) == sizeof(DummyImageViews),
                      "dummy image arrays mismatched sizes");
    RDCCOMPILE_ASSERT(ARRAY_COUNT(DummyImages) == ARRAY_COUNT(formats),
                      "dummy image arrays mismatched sizes");
    // types + 1 for cube
    RDCCOMPILE_ASSERT(ARRAY_COUNT(DummyImages[0]) == ARRAY_COUNT(types) + 1,
                      "dummy image arrays mismatched sizes");
    RDCCOMPILE_ASSERT(ARRAY_COUNT(DummyImages[0]) == ARRAY_COUNT(viewtypes),
                      "dummy image arrays mismatched sizes");
    RDCCOMPILE_ASSERT(ARRAY_COUNT(DummyWrites) == ARRAY_COUNT(DummyInfos),
                      "dummy image arrays mismatched sizes");

    CREATE_OBJECT(DummySampler, VK_FILTER_NEAREST);

    for(size_t fmt = 0; fmt < ARRAY_COUNT(formats); fmt++)
    {
      for(size_t type = 0; type < ARRAY_COUNT(types); type++)
      {
        // don't create 3D depth
        if(formats[fmt] == VK_FORMAT_D16_UNORM && types[type] == VK_IMAGE_TYPE_3D)
          continue;

        // create 1x1 image of the right size
        VkImageCreateInfo imInfo = {
            VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
            NULL,
            0,
            types[type],
            formats[fmt],
            {1, 1, 1},
            1,
            1,
            sampleCounts[type],
            VK_IMAGE_TILING_OPTIMAL,
            VK_IMAGE_USAGE_SAMPLED_BIT,
            VK_SHARING_MODE_EXCLUSIVE,
            0,
            NULL,
            VK_IMAGE_LAYOUT_UNDEFINED,
        };

        // make the 2D image cube-compatible for non-depth
        if(type == 1 && formats[fmt] != VK_FORMAT_D16_UNORM)
        {
          imInfo.arrayLayers = 6;
          imInfo.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
        }

        vkr = driver->vkCreateImage(driver->GetDev(), &imInfo, NULL, &DummyImages[fmt][type]);
        driver->CheckVkResult(vkr);

        NameVulkanObject(DummyImages[fmt][type],
                         "DummyImages[" + ToStr(fmt) + "][" + ToStr(type) + "]");

        MemoryAllocation alloc = driver->AllocateMemoryForResource(
            DummyImages[fmt][type], MemoryScope::ImmutableReplayDebug, MemoryType::GPULocal);

        if(alloc.mem == VK_NULL_HANDLE)
          return;

        vkr = driver->vkBindImageMemory(driver->GetDev(), DummyImages[fmt][type], alloc.mem,
                                        alloc.offs);
        driver->CheckVkResult(vkr);

        // don't add dummy writes/infos for depth, we just want the images and views
        if(formats[fmt] == VK_FORMAT_D16_UNORM)
          continue;

        // fill out the descriptor set write to the write binding - set will be filled out
        // on demand when we're actually using these writes.
        DummyWrites[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        DummyWrites[index].pNext = NULL;
        DummyWrites[index].dstSet = VK_NULL_HANDLE;
        DummyWrites[index].dstBinding =
            5 * uint32_t(fmt + 1) + uint32_t(type) + 1;    // 5 + RESTYPE_x
        DummyWrites[index].dstArrayElement = 0;
        DummyWrites[index].descriptorCount = 1;
        DummyWrites[index].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        DummyWrites[index].pImageInfo = &DummyInfos[index];
        DummyWrites[index].pBufferInfo = NULL;
        DummyWrites[index].pTexelBufferView = NULL;

        DummyInfos[index].sampler = Unwrap(DummySampler);
        DummyInfos[index].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        index++;
      }
    }

    // add the last one for the odd-one-out YUV texture

    DummyWrites[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    DummyWrites[index].pNext = NULL;
    DummyWrites[index].dstSet = VK_NULL_HANDLE;
    DummyWrites[index].dstBinding = 10;    // texYUV
    DummyWrites[index].dstArrayElement = 0;
    DummyWrites[index].descriptorCount = 1;
    DummyWrites[index].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    DummyWrites[index].pImageInfo = &DummyInfos[index];
    DummyWrites[index].pBufferInfo = NULL;
    DummyWrites[index].pTexelBufferView = NULL;

    DummyWrites[index + 1] = DummyWrites[index];
    DummyWrites[index + 1].dstArrayElement = 1;

    DummyInfos[index].sampler = Unwrap(DummySampler);
    DummyInfos[index].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    DummyInfos[index + 1].sampler = Unwrap(DummySampler);
    DummyInfos[index + 1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

    RDCASSERT(index + 1 < (int)ARRAY_COUNT(DummyInfos));

    // align up for the dummy buffer
    {
      VkBufferCreateInfo bufInfo = {
          VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,     NULL, 0, 16,
          VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT,
      };

      vkr = driver->vkCreateBuffer(driver->GetDev(), &bufInfo, NULL, &DummyBuffer);
      driver->CheckVkResult(vkr);

      MemoryAllocation alloc = driver->AllocateMemoryForResource(
          DummyBuffer, MemoryScope::ImmutableReplayDebug, MemoryType::GPULocal);

      if(alloc.mem == VK_NULL_HANDLE)
        return;

      vkr = driver->vkBindBufferMemory(driver->GetDev(), DummyBuffer, alloc.mem, alloc.offs);
      driver->CheckVkResult(vkr);
    }

    // now that the image memory is bound, we can create the image views and fill the descriptor
    // set writes.
    index = 0;
    for(size_t fmt = 0; fmt < ARRAY_COUNT(formats); fmt++)
    {
      for(size_t type = 0; type < ARRAY_COUNT(viewtypes); type++)
      {
        size_t imType = type;

        // the cubemap view re-uses the 2D image
        bool cube = false;
        if(viewtypes[type] == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY ||
           viewtypes[type] == VK_IMAGE_VIEW_TYPE_CUBE)
        {
          imType = 1;
          cube = true;
        }

        // don't create 3D depth or cubes
        if(formats[fmt] == VK_FORMAT_D16_UNORM && (viewtypes[type] == VK_IMAGE_VIEW_TYPE_3D || cube))
          continue;

        VkImageViewCreateInfo viewInfo = {
            VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
            NULL,
            0,
            DummyImages[fmt][imType],
            viewtypes[type],
            formats[fmt],
            {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
             VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
            {
                VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1,
            },
        };

        if(formats[fmt] == VK_FORMAT_D16_UNORM)
          viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;

        if(cube)
          viewInfo.subresourceRange.layerCount = 6;

        vkr = driver->vkCreateImageView(driver->GetDev(), &viewInfo, NULL,
                                        &DummyImageViews[fmt][type]);
        driver->CheckVkResult(vkr);

        NameVulkanObject(DummyImageViews[fmt][type],
                         "DummyImageViews[" + ToStr(fmt) + "][" + ToStr(type) + "]");

        // the cubemap view we don't create an info for it, and the image is already transitioned
        if(cube)
          continue;

        // need to update image layout into valid state
        VkImageMemoryBarrier barrier = {
            VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            NULL,
            0,
            VK_ACCESS_SHADER_READ_BIT,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
            VK_QUEUE_FAMILY_IGNORED,
            VK_QUEUE_FAMILY_IGNORED,
            Unwrap(DummyImages[fmt][imType]),
            {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS},
        };

        if(formats[fmt] == VK_FORMAT_D16_UNORM)
          barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;

        DoPipelineBarrier(cmd, 1, &barrier);

        if(formats[fmt] == VK_FORMAT_D16_UNORM)
          continue;

        RDCASSERT((size_t)index < ARRAY_COUNT(DummyInfos), index);

        DummyInfos[index].imageView = Unwrap(DummyImageViews[fmt][type]);

        index++;
      }
    }

    // duplicate 2D dummy image into YUV
    DummyInfos[index].imageView = DummyInfos[1].imageView;
    DummyInfos[index + 1].imageView = DummyInfos[1].imageView;

    RDCASSERT(index + 1 < (int)ARRAY_COUNT(DummyInfos));

    if(DummyBuffer != VK_NULL_HANDLE)
    {
      VkFormat bufViewTypes[] = {
          VK_FORMAT_R32G32B32A32_SFLOAT, VK_FORMAT_R32G32B32A32_UINT, VK_FORMAT_R32G32B32A32_SINT,
      };
      for(size_t i = 0; i < ARRAY_COUNT(bufViewTypes); i++)
      {
        VkBufferViewCreateInfo viewInfo = {
            VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO, NULL, 0, DummyBuffer, bufViewTypes[i], 0, 16,
        };

        vkr = driver->vkCreateBufferView(driver->GetDev(), &viewInfo, NULL, &DummyBufferView[i]);
        driver->CheckVkResult(vkr);
      }
    }

    ObjDisp(cmd)->EndCommandBuffer(Unwrap(cmd));
  }
}

void VulkanReplay::TextureRendering::Destroy(WrappedVulkan *driver)
{
  if(DescSetLayout == VK_NULL_HANDLE)
    return;

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), DescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), PipeLayout, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), Pipeline, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), BlendPipeline, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), F16Pipeline, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), F32Pipeline, NULL);
  for(size_t f = 0; f < 3; f++)
    for(size_t i = 0; i < 3; i++)
      for(size_t g = 0; g < 2; g++)
        driver->vkDestroyPipeline(driver->GetDev(), RemapPipeline[f][i][g], NULL);

  driver->vkDestroyPipeline(driver->GetDev(), PipelineGreenOnly, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), F16PipelineGreenOnly, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), F32PipelineGreenOnly, NULL);
  UBO.Destroy();
  HeatmapUBO.Destroy();

  driver->vkDestroySampler(driver->GetDev(), PointSampler, NULL);
  driver->vkDestroySampler(driver->GetDev(), LinearSampler, NULL);

  for(size_t fmt = 0; fmt < ARRAY_COUNT(DummyImages); fmt++)
  {
    for(size_t type = 0; type < ARRAY_COUNT(DummyImages[0]); type++)
    {
      driver->vkDestroyImageView(driver->GetDev(), DummyImageViews[fmt][type], NULL);
      driver->vkDestroyImage(driver->GetDev(), DummyImages[fmt][type], NULL);
    }
  }

  for(size_t fmt = 0; fmt < ARRAY_COUNT(DummyBufferView); fmt++)
    driver->vkDestroyBufferView(driver->GetDev(), DummyBufferView[fmt], NULL);
  driver->vkDestroyBuffer(driver->GetDev(), DummyBuffer, NULL);

  driver->vkDestroySampler(driver->GetDev(), DummySampler, NULL);
}

void VulkanReplay::OverlayRendering::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VulkanShaderCache *shaderCache = driver->GetShaderCache();

  VkRenderPass SRGBA8RP = VK_NULL_HANDLE;
  VkRenderPass SRGBA8MSRP = VK_NULL_HANDLE;

  CREATE_OBJECT(SRGBA8RP, VK_FORMAT_R8G8B8A8_SRGB);
  CREATE_OBJECT(SRGBA8MSRP, VK_FORMAT_R8G8B8A8_SRGB, VULKAN_MESH_VIEW_SAMPLES);

  CREATE_OBJECT(m_CheckerDescSetLayout,
                {{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL}});

  CREATE_OBJECT(m_QuadDescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  CREATE_OBJECT(m_TriSizeDescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL},
                    {2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  CREATE_OBJECT(m_CheckerPipeLayout, m_CheckerDescSetLayout, 0);
  CREATE_OBJECT(m_QuadResolvePipeLayout, m_QuadDescSetLayout, 0);
  CREATE_OBJECT(m_TriSizePipeLayout, m_TriSizeDescSetLayout, 0);
  CREATE_OBJECT(m_QuadDescSet, descriptorPool, m_QuadDescSetLayout);
  CREATE_OBJECT(m_TriSizeDescSet, descriptorPool, m_TriSizeDescSetLayout);
  CREATE_OBJECT(m_CheckerDescSet, descriptorPool, m_CheckerDescSetLayout);

  m_CheckerUBO.Create(driver, driver->GetDev(), 128, 10, 0);
  RDCCOMPILE_ASSERT(sizeof(CheckerboardUBOData) <= 128, "checkerboard UBO size");

  m_TriSizeUBO.Create(driver, driver->GetDev(), sizeof(Vec4f), 4096, 0);

  ConciseGraphicsPipeline pipeInfo = {
      SRGBA8RP,
      m_CheckerPipeLayout,
      shaderCache->GetBuiltinModule(BuiltinShader::BlitVS),
      shaderCache->GetBuiltinModule(BuiltinShader::CheckerboardFS),
      {VK_DYNAMIC_STATE_VIEWPORT},
      VK_SAMPLE_COUNT_1_BIT,
      false,    // sampleRateShading
      false,    // depthEnable
      false,    // stencilEnable
      VK_STENCIL_OP_KEEP,
      true,     // colourOutput
      false,    // blendEnable
      VK_BLEND_FACTOR_SRC_ALPHA,
      VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
      0xf,    // writeMask
  };

  CREATE_OBJECT(m_CheckerPipeline, pipeInfo);

  pipeInfo.renderPass = SRGBA8MSRP;
  pipeInfo.sampleCount = VULKAN_MESH_VIEW_SAMPLES;

  CREATE_OBJECT(m_CheckerMSAAPipeline, pipeInfo);

  uint32_t samplesHandled = 0;

  RDCCOMPILE_ASSERT(ARRAY_COUNT(m_CheckerF16Pipeline) == ARRAY_COUNT(m_QuadResolvePipeline),
                    "Arrays are mismatched in size!");

  uint32_t supportedSampleCounts = driver->GetDeviceProps().limits.framebufferColorSampleCounts;

  for(size_t i = 0; i < ARRAY_COUNT(m_CheckerF16Pipeline); i++)
  {
    VkSampleCountFlagBits samples = VkSampleCountFlagBits(1 << i);

    if((supportedSampleCounts & (uint32_t)samples) == 0)
      continue;

    VkRenderPass RGBA16MSRP = VK_NULL_HANDLE;

    CREATE_OBJECT(RGBA16MSRP, VK_FORMAT_R16G16B16A16_SFLOAT, samples);

    if(RGBA16MSRP != VK_NULL_HANDLE)
      samplesHandled |= (uint32_t)samples;
    else
      continue;

    // if we this sample count is supported then create a pipeline
    pipeInfo.renderPass = RGBA16MSRP;
    pipeInfo.sampleCount = VkSampleCountFlagBits(1 << i);

    // set up outline pipeline configuration
    pipeInfo.blendEnable = true;
    pipeInfo.fragment = shaderCache->GetBuiltinModule(BuiltinShader::CheckerboardFS);
    pipeInfo.pipeLayout = m_CheckerPipeLayout;

    CREATE_OBJECT(m_CheckerF16Pipeline[i], pipeInfo);

    // set up quad resolve pipeline configuration
    pipeInfo.blendEnable = false;
    pipeInfo.fragment = shaderCache->GetBuiltinModule(BuiltinShader::QuadResolveFS);
    pipeInfo.pipeLayout = m_QuadResolvePipeLayout;

    if(pipeInfo.fragment != VK_NULL_HANDLE &&
       shaderCache->GetBuiltinModule(BuiltinShader::QuadWriteFS) != VK_NULL_HANDLE)
    {
      CREATE_OBJECT(m_QuadResolvePipeline[i], pipeInfo);
    }

    driver->vkDestroyRenderPass(driver->GetDev(), RGBA16MSRP, NULL);
  }

  RDCASSERTEQUAL((uint32_t)driver->GetDeviceProps().limits.framebufferColorSampleCounts,
                 samplesHandled);

  VkDescriptorBufferInfo checkerboard = {};
  m_CheckerUBO.FillDescriptor(checkerboard);

  VkWriteDescriptorSet writes[] = {
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(m_CheckerDescSet), 0, 0, 1,
       VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, NULL, &checkerboard, NULL},
  };

  VkDevice dev = driver->GetDev();

  ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), ARRAY_COUNT(writes), writes, 0, NULL);

  driver->vkDestroyRenderPass(driver->GetDev(), SRGBA8RP, NULL);
  driver->vkDestroyRenderPass(driver->GetDev(), SRGBA8MSRP, NULL);
}

void VulkanReplay::OverlayRendering::Destroy(WrappedVulkan *driver)
{
  if(ImageMem == VK_NULL_HANDLE)
    return;

  driver->vkFreeMemory(driver->GetDev(), ImageMem, NULL);
  driver->vkDestroyImage(driver->GetDev(), Image, NULL);
  driver->vkDestroyImageView(driver->GetDev(), ImageView, NULL);
  driver->vkDestroyFramebuffer(driver->GetDev(), NoDepthFB, NULL);
  driver->vkDestroyRenderPass(driver->GetDev(), NoDepthRP, NULL);

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), m_QuadDescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), m_QuadResolvePipeLayout, NULL);
  for(size_t i = 0; i < ARRAY_COUNT(m_QuadResolvePipeline); i++)
    driver->vkDestroyPipeline(driver->GetDev(), m_QuadResolvePipeline[i], NULL);

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), m_CheckerDescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), m_CheckerPipeLayout, NULL);
  for(size_t i = 0; i < ARRAY_COUNT(m_CheckerF16Pipeline); i++)
    driver->vkDestroyPipeline(driver->GetDev(), m_CheckerF16Pipeline[i], NULL);
  driver->vkDestroyPipeline(driver->GetDev(), m_CheckerPipeline, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), m_CheckerMSAAPipeline, NULL);

  m_CheckerUBO.Destroy();

  m_TriSizeUBO.Destroy();
  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), m_TriSizeDescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), m_TriSizePipeLayout, NULL);
}

void VulkanReplay::MeshRendering::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  CREATE_OBJECT(DescSetLayout,
                {{0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_ALL, NULL}});

  CREATE_OBJECT(PipeLayout, DescSetLayout, 0);
  CREATE_OBJECT(DescSet, descriptorPool, DescSetLayout);

  UBO.Create(driver, driver->GetDev(), sizeof(MeshUBOData), 16, 0);
  BBoxVB.Create(driver, driver->GetDev(), sizeof(Vec4f) * 128, 16, GPUBuffer::eGPUBufferVBuffer);

  Vec4f TLN = Vec4f(-1.0f, 1.0f, 0.0f, 1.0f);    // TopLeftNear, etc...
  Vec4f TRN = Vec4f(1.0f, 1.0f, 0.0f, 1.0f);
  Vec4f BLN = Vec4f(-1.0f, -1.0f, 0.0f, 1.0f);
  Vec4f BRN = Vec4f(1.0f, -1.0f, 0.0f, 1.0f);

  Vec4f TLF = Vec4f(-1.0f, 1.0f, 1.0f, 1.0f);
  Vec4f TRF = Vec4f(1.0f, 1.0f, 1.0f, 1.0f);
  Vec4f BLF = Vec4f(-1.0f, -1.0f, 1.0f, 1.0f);
  Vec4f BRF = Vec4f(1.0f, -1.0f, 1.0f, 1.0f);

  Vec4f axisFrustum[] = {
      // axis marker vertices
      Vec4f(0.0f, 0.0f, 0.0f, 1.0f), Vec4f(1.0f, 0.0f, 0.0f, 1.0f), Vec4f(0.0f, 0.0f, 0.0f, 1.0f),
      Vec4f(0.0f, 1.0f, 0.0f, 1.0f), Vec4f(0.0f, 0.0f, 0.0f, 1.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f),

      // frustum vertices
      TLN, TRN, TRN, BRN, BRN, BLN, BLN, TLN,

      TLN, TLF, TRN, TRF, BLN, BLF, BRN, BRF,

      TLF, TRF, TRF, BRF, BRF, BLF, BLF, TLF,
  };

  // doesn't need to be ring'd as it's immutable
  AxisFrustumVB.Create(driver, driver->GetDev(), sizeof(axisFrustum), 1,
                       GPUBuffer::eGPUBufferVBuffer);

  Vec4f *axisData = (Vec4f *)AxisFrustumVB.Map();

  if(axisData)
    memcpy(axisData, axisFrustum, sizeof(axisFrustum));

  AxisFrustumVB.Unmap();

  VkDescriptorBufferInfo meshrender = {};

  UBO.FillDescriptor(meshrender);

  VkWriteDescriptorSet writes[] = {
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(DescSet), 0, 0, 1,
       VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, NULL, &meshrender, NULL},
  };

  VkDevice dev = driver->GetDev();

  ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), ARRAY_COUNT(writes), writes, 0, NULL);
}

void VulkanReplay::MeshRendering::Destroy(WrappedVulkan *driver)
{
  if(DescSetLayout == VK_NULL_HANDLE)
    return;

  UBO.Destroy();
  BBoxVB.Destroy();
  AxisFrustumVB.Destroy();

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), DescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), PipeLayout, NULL);
}

void VulkanReplay::VertexPicking::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VulkanShaderCache *shaderCache = driver->GetShaderCache();

  CREATE_OBJECT(DescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  CREATE_OBJECT(Layout, DescSetLayout, 0);
  CREATE_OBJECT(DescSet, descriptorPool, DescSetLayout);

  // sizes are always 0 so that these buffers are created on demand
  IBSize = 0;
  VBSize = 0;

  UBO.Create(driver, driver->GetDev(), 128, 1, 0);
  RDCCOMPILE_ASSERT(sizeof(MeshPickUBOData) <= 128, "mesh pick UBO size");

  const size_t meshPickResultSize = MaxMeshPicks * sizeof(FloatVector) + sizeof(uint32_t);

  Result.Create(driver, driver->GetDev(), meshPickResultSize, 1,
                GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
  ResultReadback.Create(driver, driver->GetDev(), meshPickResultSize, 1,
                        GPUBuffer::eGPUBufferReadback);

  CREATE_OBJECT(Pipeline, Layout, shaderCache->GetBuiltinModule(BuiltinShader::MeshCS));

  VkDescriptorBufferInfo vertexpickUBO = {};
  VkDescriptorBufferInfo vertexpickResult = {};

  UBO.FillDescriptor(vertexpickUBO);
  Result.FillDescriptor(vertexpickResult);

  VkWriteDescriptorSet writes[] = {
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(DescSet), 0, 0, 1,
       VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, NULL, &vertexpickUBO, NULL},
      {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, NULL, Unwrap(DescSet), 3, 0, 1,
       VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NULL, &vertexpickResult, NULL},
  };

  VkDevice dev = driver->GetDev();

  ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), ARRAY_COUNT(writes), writes, 0, NULL);
}

void VulkanReplay::VertexPicking::Destroy(WrappedVulkan *driver)
{
  if(DescSetLayout == VK_NULL_HANDLE)
    return;

  UBO.Destroy();
  IB.Destroy();
  IBUpload.Destroy();
  VB.Destroy();
  VBUpload.Destroy();
  Result.Destroy();
  ResultReadback.Destroy();

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), DescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), Layout, NULL);
  driver->vkDestroyPipeline(driver->GetDev(), Pipeline, NULL);
}

void VulkanReplay::PixelPicking::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VkResult vkr = VK_SUCCESS;

  VkImageCreateInfo imInfo = {
      VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
      NULL,
      0,
      VK_IMAGE_TYPE_2D,
      VK_FORMAT_R32G32B32A32_SFLOAT,
      {1, 1, 1},
      1,
      1,
      VK_SAMPLE_COUNT_1_BIT,
      VK_IMAGE_TILING_OPTIMAL,
      VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
      VK_SHARING_MODE_EXCLUSIVE,
      0,
      NULL,
      VK_IMAGE_LAYOUT_UNDEFINED,
  };

  vkr = driver->vkCreateImage(driver->GetDev(), &imInfo, NULL, &Image);
  driver->CheckVkResult(vkr);

  NameVulkanObject(Image, "PixelPick.Image");

  VkMemoryRequirements mrq = {0};
  driver->vkGetImageMemoryRequirements(driver->GetDev(), Image, &mrq);

  // allocate readback memory
  VkMemoryAllocateInfo allocInfo = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size,
      driver->GetGPULocalMemoryIndex(mrq.memoryTypeBits),
  };

  vkr = driver->vkAllocateMemory(driver->GetDev(), &allocInfo, NULL, &ImageMem);
  driver->CheckVkResult(vkr);

  vkr = driver->vkBindImageMemory(driver->GetDev(), Image, ImageMem, 0);
  driver->CheckVkResult(vkr);

  VkImageViewCreateInfo viewInfo = {
      VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
      NULL,
      0,
      Image,
      VK_IMAGE_VIEW_TYPE_2D,
      VK_FORMAT_R32G32B32A32_SFLOAT,
      {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
       VK_COMPONENT_SWIZZLE_IDENTITY},
      {
          VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1,
      },
  };

  vkr = driver->vkCreateImageView(driver->GetDev(), &viewInfo, NULL, &ImageView);
  driver->CheckVkResult(vkr);

  NameVulkanObject(ImageView, "PixelPick.ImageView");

  // need to update image layout into valid state

  VkCommandBuffer cmd = driver->GetNextCmd();

  if(cmd == VK_NULL_HANDLE)
    return;

  VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                        VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

  vkr = ObjDisp(cmd)->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
  driver->CheckVkResult(vkr);

  VkImageMemoryBarrier barrier = {
      VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
      NULL,
      0,
      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
      VK_IMAGE_LAYOUT_UNDEFINED,
      VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
      VK_QUEUE_FAMILY_IGNORED,
      VK_QUEUE_FAMILY_IGNORED,
      Unwrap(Image),
      {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1},
  };

  DoPipelineBarrier(cmd, 1, &barrier);

  ObjDisp(cmd)->EndCommandBuffer(Unwrap(cmd));

  CREATE_OBJECT(RP, VK_FORMAT_R32G32B32A32_SFLOAT);

  // create framebuffer
  VkFramebufferCreateInfo fbinfo = {
      VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, NULL, 0, RP, 1, &ImageView, 1, 1, 1,
  };

  vkr = driver->vkCreateFramebuffer(driver->GetDev(), &fbinfo, NULL, &FB);
  driver->CheckVkResult(vkr);

  // since we always sync for readback, doesn't need to be ring'd
  ReadbackBuffer.Create(driver, driver->GetDev(), sizeof(float) * 4, 1,
                        GPUBuffer::eGPUBufferReadback);
}

void VulkanReplay::PixelPicking::Destroy(WrappedVulkan *driver)
{
  if(Image == VK_NULL_HANDLE)
    return;

  driver->vkDestroyImage(driver->GetDev(), Image, NULL);
  driver->vkFreeMemory(driver->GetDev(), ImageMem, NULL);
  driver->vkDestroyImageView(driver->GetDev(), ImageView, NULL);
  ReadbackBuffer.Destroy();
  driver->vkDestroyFramebuffer(driver->GetDev(), FB, NULL);
  driver->vkDestroyRenderPass(driver->GetDev(), RP, NULL);
}

void VulkanReplay::PixelHistory::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  CREATE_OBJECT(MSCopyDescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  VkResult vkr = VK_SUCCESS;
  VkDescriptorPoolSize descPoolTypes[] = {
      {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 32},
  };

  VkDescriptorPoolCreateInfo descPoolInfo = {
      VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
      NULL,
      0,
      32,
      ARRAY_COUNT(descPoolTypes),
      &descPoolTypes[0],
  };

  // create descriptor pool
  vkr = driver->vkCreateDescriptorPool(driver->GetDev(), &descPoolInfo, NULL, &MSCopyDescPool);
  driver->CheckVkResult(vkr);

  CREATE_OBJECT(MSCopyPipeLayout, MSCopyDescSetLayout, 32);
  CREATE_OBJECT(MSCopyPipe, MSCopyPipeLayout,
                driver->GetShaderCache()->GetBuiltinModule(BuiltinShader::PixelHistoryMSCopyCS));
  CREATE_OBJECT(MSCopyDepthPipe, MSCopyPipeLayout,
                driver->GetShaderCache()->GetBuiltinModule(BuiltinShader::PixelHistoryMSCopyDepthCS));
}

void VulkanReplay::PixelHistory::Destroy(WrappedVulkan *driver)
{
  if(MSCopyPipe != VK_NULL_HANDLE)
    driver->vkDestroyPipeline(driver->GetDev(), MSCopyPipe, NULL);
  if(MSCopyPipeLayout != VK_NULL_HANDLE)
    driver->vkDestroyPipelineLayout(driver->GetDev(), MSCopyPipeLayout, NULL);
  if(MSCopyDescSetLayout != VK_NULL_HANDLE)
    driver->vkDestroyDescriptorSetLayout(driver->GetDev(), MSCopyDescSetLayout, NULL);
  if(MSCopyDescPool != VK_NULL_HANDLE)
    driver->vkDestroyDescriptorPool(driver->GetDev(), MSCopyDescPool, NULL);
}

void VulkanReplay::HistogramMinMax::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  VulkanShaderCache *shaderCache = driver->GetShaderCache();

  shaderCache->SetCaching(true);

  rdcstr glsl;

  CREATE_OBJECT(m_HistogramDescSetLayout,
                {
                    {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {6, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {7, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {8, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {9, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {10, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2, VK_SHADER_STAGE_ALL, NULL},
                    {11, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {12, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {13, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {14, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {16, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {17, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {18, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                    {19, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_ALL, NULL},
                });

  CREATE_OBJECT(m_HistogramPipeLayout, m_HistogramDescSetLayout, 0);

  for(size_t i = 0; i < ARRAY_COUNT(m_HistogramDescSet); i++)
    CREATE_OBJECT(m_HistogramDescSet[i], descriptorPool, m_HistogramDescSetLayout);

  rdcspv::CompilationSettings compileSettings;
  compileSettings.lang = rdcspv::InputLanguage::VulkanGLSL;
  compileSettings.stage = rdcspv::ShaderStage::Compute;

  // type max is one higher than the last RESTYPE, and RESTYPES are 1-indexed
  RDCCOMPILE_ASSERT(RESTYPE_TEXTYPEMAX == ARRAY_COUNT(m_MinMaxTilePipe),
                    "RESTYPE values don't match formats for dummy images");

  RDCCOMPILE_ASSERT(ARRAY_COUNT(m_MinMaxTilePipe) == arraydim<BuiltinShaderTextureType>(),
                    "Array size doesn't match parameter enum");
  RDCCOMPILE_ASSERT(ARRAY_COUNT(m_MinMaxTilePipe[0]) == arraydim<BuiltinShaderBaseType>(),
                    "Array size doesn't match parameter enum");

  for(BuiltinShaderTextureType t = BuiltinShaderTextureType::First;
      t < BuiltinShaderTextureType::Count; ++t)
  {
    for(BuiltinShaderBaseType f = BuiltinShaderBaseType::First; f < BuiltinShaderBaseType::Count; ++f)
    {
      CREATE_OBJECT(m_HistogramPipe[(size_t)t][(size_t)f], m_HistogramPipeLayout,
                    shaderCache->GetBuiltinModule(BuiltinShader::HistogramCS, f, t));
      CREATE_OBJECT(m_MinMaxTilePipe[(size_t)t][(size_t)f], m_HistogramPipeLayout,
                    shaderCache->GetBuiltinModule(BuiltinShader::MinMaxTileCS, f, t));

      if(t == BuiltinShaderTextureType::First)
      {
        CREATE_OBJECT(m_MinMaxResultPipe[(size_t)f], m_HistogramPipeLayout,
                      shaderCache->GetBuiltinModule(BuiltinShader::MinMaxResultCS, f));
      }
    }
  }

  shaderCache->SetCaching(false);

  const uint32_t maxTexDim = 16384;
  const uint32_t blockPixSize = HGRAM_PIXELS_PER_TILE * HGRAM_TILES_PER_BLOCK;
  const uint32_t maxBlocksNeeded = (maxTexDim * maxTexDim) / (blockPixSize * blockPixSize);

  const size_t byteSize =
      2 * sizeof(Vec4f) * HGRAM_TILES_PER_BLOCK * HGRAM_TILES_PER_BLOCK * maxBlocksNeeded;

  m_MinMaxTileResult.Create(driver, driver->GetDev(), byteSize, 1,
                            GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
  m_MinMaxResult.Create(driver, driver->GetDev(), sizeof(Vec4f) * 2, 1,
                        GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
  m_MinMaxReadback.Create(driver, driver->GetDev(), sizeof(Vec4f) * 2, 1,
                          GPUBuffer::eGPUBufferReadback);
  m_HistogramBuf.Create(driver, driver->GetDev(), sizeof(uint32_t) * HGRAM_NUM_BUCKETS, 1,
                        GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);
  m_HistogramReadback.Create(driver, driver->GetDev(), sizeof(uint32_t) * HGRAM_NUM_BUCKETS, 1,
                             GPUBuffer::eGPUBufferReadback);

  // don't need to ring this, as we hard-sync for readback anyway
  m_HistogramUBO.Create(driver, driver->GetDev(), sizeof(HistogramUBOData), 1, 0);
}

void VulkanReplay::HistogramMinMax::Destroy(WrappedVulkan *driver)
{
  if(m_HistogramDescSetLayout == VK_NULL_HANDLE)
    return;

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), m_HistogramDescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), m_HistogramPipeLayout, NULL);

  for(size_t t = 1; t < ARRAY_COUNT(m_MinMaxTilePipe); t++)
  {
    for(size_t f = 0; f < ARRAY_COUNT(m_MinMaxTilePipe[0]); f++)
    {
      driver->vkDestroyPipeline(driver->GetDev(), m_MinMaxTilePipe[t][f], NULL);
      driver->vkDestroyPipeline(driver->GetDev(), m_HistogramPipe[t][f], NULL);
      if(t == 1)
        driver->vkDestroyPipeline(driver->GetDev(), m_MinMaxResultPipe[f], NULL);
    }
  }

  m_MinMaxTileResult.Destroy();
  m_MinMaxResult.Destroy();
  m_MinMaxReadback.Destroy();
  m_HistogramBuf.Destroy();
  m_HistogramReadback.Destroy();
  m_HistogramUBO.Destroy();
}

void VulkanReplay::PostVS::Destroy(WrappedVulkan *driver)
{
  if(XFBQueryPool != VK_NULL_HANDLE)
    driver->vkDestroyQueryPool(driver->GetDev(), XFBQueryPool, NULL);
}

void VulkanReplay::Feedback::Destroy(WrappedVulkan *driver)
{
  FeedbackBuffer.Destroy();
}

void ShaderDebugData::Init(WrappedVulkan *driver, VkDescriptorPool descriptorPool)
{
  // should match the enum ShaderDebugBind
  CREATE_OBJECT(
      DescSetLayout,
      {
          // ShaderDebugBind::Tex1D
          {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Tex2D
          {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Tex3D
          {3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Tex2DMS
          {4, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::TexCube
          {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Buffer
          {6, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Sampler
          {7, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::Constants
          {8, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, NULL},
          // ShaderDebugBind::MathResult
          {9, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1,
           VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT, NULL},
      });

  CREATE_OBJECT(PipeLayout, DescSetLayout, sizeof(Vec4f) * 6 + sizeof(uint32_t));

  CREATE_OBJECT(DescSet, descriptorPool, DescSetLayout);

  VkResult vkr = VK_SUCCESS;

  VkImageCreateInfo imInfo = {
      VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
      NULL,
      0,
      VK_IMAGE_TYPE_2D,
      VK_FORMAT_R32G32B32A32_SFLOAT,
      {1, 1, 1},
      1,
      1,
      VK_SAMPLE_COUNT_1_BIT,
      VK_IMAGE_TILING_OPTIMAL,
      VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
          VK_IMAGE_USAGE_TRANSFER_DST_BIT,
      VK_SHARING_MODE_EXCLUSIVE,
      0,
      NULL,
      VK_IMAGE_LAYOUT_UNDEFINED,
  };

  vkr = driver->vkCreateImage(driver->GetDev(), &imInfo, NULL, &Image);
  driver->CheckVkResult(vkr);

  NameVulkanObject(Image, "ShaderDebugData.Image");

  VkMemoryRequirements mrq = {0};
  driver->vkGetImageMemoryRequirements(driver->GetDev(), Image, &mrq);

  // allocate readback memory
  VkMemoryAllocateInfo allocInfo = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size,
      driver->GetGPULocalMemoryIndex(mrq.memoryTypeBits),
  };

  vkr = driver->vkAllocateMemory(driver->GetDev(), &allocInfo, NULL, &ImageMemory);
  driver->CheckVkResult(vkr);

  vkr = driver->vkBindImageMemory(driver->GetDev(), Image, ImageMemory, 0);
  driver->CheckVkResult(vkr);

  VkImageViewCreateInfo viewInfo = {
      VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
      NULL,
      0,
      Image,
      VK_IMAGE_VIEW_TYPE_2D,
      VK_FORMAT_R32G32B32A32_SFLOAT,
      {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
       VK_COMPONENT_SWIZZLE_IDENTITY},
      {
          VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1,
      },
  };

  vkr = driver->vkCreateImageView(driver->GetDev(), &viewInfo, NULL, &ImageView);
  driver->CheckVkResult(vkr);

  NameVulkanObject(ImageView, "ShaderDebugData.ImageView");

  VkAttachmentDescription attDesc = {
      0,
      VK_FORMAT_R32G32B32A32_SFLOAT,
      VK_SAMPLE_COUNT_1_BIT,
      VK_ATTACHMENT_LOAD_OP_CLEAR,
      VK_ATTACHMENT_STORE_OP_STORE,
      VK_ATTACHMENT_LOAD_OP_DONT_CARE,
      VK_ATTACHMENT_STORE_OP_DONT_CARE,
      VK_IMAGE_LAYOUT_UNDEFINED,
      VK_IMAGE_LAYOUT_GENERAL,
  };

  VkAttachmentReference attRef = {0, VK_IMAGE_LAYOUT_GENERAL};

  VkSubpassDescription sub = {
      0, VK_PIPELINE_BIND_POINT_GRAPHICS, 0, NULL, 1, &attRef,
  };

  VkSubpassDependency deps[2] = {
      {
          VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
          VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_READ_BIT,
          VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0,
      },
      {
          0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
          VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
          VK_ACCESS_TRANSFER_READ_BIT, 0,
      },
  };

  VkRenderPassCreateInfo rpinfo = {
      VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, NULL, 0, 1, &attDesc, 1, &sub, 2, deps,
  };

  vkr = driver->vkCreateRenderPass(driver->GetDev(), &rpinfo, NULL, &RenderPass);
  if(vkr != VK_SUCCESS)
    RDCERR("Failed to create shader debug render pass: %s", ToStr(vkr).c_str());

  // create framebuffer
  VkFramebufferCreateInfo fbinfo = {
      VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, NULL, 0, RenderPass, 1, &ImageView, 1, 1, 1,
  };

  vkr = driver->vkCreateFramebuffer(driver->GetDev(), &fbinfo, NULL, &Framebuffer);
  driver->CheckVkResult(vkr);

  MathResult.Create(driver, driver->GetDev(), sizeof(Vec4f) * 4, 1,
                    GPUBuffer::eGPUBufferGPULocal | GPUBuffer::eGPUBufferSSBO);

  // don't need to ring this, as we hard-sync for readback anyway
  ReadbackBuffer.Create(driver, driver->GetDev(), sizeof(Vec4f) * 4, 1,
                        GPUBuffer::eGPUBufferReadback);
  ConstantsBuffer.Create(driver, driver->GetDev(), 1024, 1, 0);
}

void ShaderDebugData::Destroy(WrappedVulkan *driver)
{
  ConstantsBuffer.Destroy();
  ReadbackBuffer.Destroy();

  for(size_t i = 0; i < ARRAY_COUNT(MathPipe); i++)
    driver->vkDestroyPipeline(driver->GetDev(), MathPipe[i], NULL);

  driver->vkDestroyDescriptorSetLayout(driver->GetDev(), DescSetLayout, NULL);
  driver->vkDestroyPipelineLayout(driver->GetDev(), PipeLayout, NULL);

  driver->vkDestroyImage(driver->GetDev(), Image, NULL);
  driver->vkFreeMemory(driver->GetDev(), ImageMemory, NULL);
  driver->vkDestroyImageView(driver->GetDev(), ImageView, NULL);
  driver->vkDestroyFramebuffer(driver->GetDev(), Framebuffer, NULL);
  driver->vkDestroyRenderPass(driver->GetDev(), RenderPass, NULL);

  // one module each for float, uint, sint.
  for(size_t i = 0; i < ARRAY_COUNT(Module); i++)
    driver->vkDestroyShaderModule(driver->GetDev(), Module[i], NULL);

  for(auto it = m_Pipelines.begin(); it != m_Pipelines.end(); it++)
    driver->vkDestroyPipeline(driver->GetDev(), it->second, NULL);
}
