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

#include <float.h>
#include <math.h>
#include <algorithm>
#include "core/settings.h"
#include "data/glsl_shaders.h"
#include "driver/shaders/spirv/spirv_common.h"
#include "driver/shaders/spirv/spirv_gen.h"
#include "maths/formatpacking.h"
#include "maths/matrix.h"
#include "strings/string_utils.h"
#include "vk_core.h"
#include "vk_debug.h"
#include "vk_replay.h"
#include "vk_shader_cache.h"

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

RDOC_EXTERN_CONFIG(bool, Vulkan_Debug_SingleSubmitFlushing);

struct VulkanQuadOverdrawCallback : public VulkanActionCallback
{
  VulkanQuadOverdrawCallback(WrappedVulkan *vk, VkDescriptorSetLayout descSetLayout,
                             VkDescriptorSet descSet, VkDescriptorSetLayout descBufLayout,
                             const rdcarray<uint32_t> &events, bool multiview)
      : m_pDriver(vk),
        m_DescSetLayout(descSetLayout),
        m_DescSet(descSet),
        m_DescBufLayout(descBufLayout),
        m_Events(events),
        m_Multiview(multiview)
  {
    m_pDriver->SetActionCB(this);
  }
  ~VulkanQuadOverdrawCallback()
  {
    m_pDriver->SetActionCB(NULL);

    VkDevice dev = m_pDriver->GetDev();

    for(auto it = m_PipelineCache.begin(); it != m_PipelineCache.end(); ++it)
    {
      m_pDriver->vkDestroyPipeline(dev, it->second.pipe, NULL);
      m_pDriver->vkDestroyPipelineLayout(dev, it->second.pipeLayout, NULL);
    }

    for(auto it = m_ShaderCache.begin(); it != m_ShaderCache.end(); ++it)
    {
      if(it->second.shad != VK_NULL_HANDLE)
        m_pDriver->vkDestroyShaderEXT(dev, it->second.shad, NULL);
      m_pDriver->vkDestroyPipelineLayout(dev, it->second.pipeLayout, NULL);
    }
  }
  void PreDraw(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd)
  {
    if(!m_Events.contains(eid))
      return;

    // we customise the pipeline to disable framebuffer writes, but perform normal testing
    // and substitute our quad calculation fragment shader that writes to a storage image
    // that is bound in a new descriptor set.

    VkResult vkr = VK_SUCCESS;

    m_PrevState = m_pDriver->GetCmdRenderState();
    VulkanRenderState &pipestate = m_pDriver->GetCmdRenderState();

    // check cache first
    CachedPipeline pipe = m_PipelineCache[pipestate.graphics.pipeline];
    CachedShader shad = m_ShaderCache[pipestate.shaderObjects[4]];

    bool descBuf = false;

    // if we don't get a hit, create a modified pipeline
    if(pipestate.graphics.shaderObject ? shad.shad == VK_NULL_HANDLE : pipe.pipe == VK_NULL_HANDLE)
    {
      const VulkanCreationInfo::Pipeline &p =
          m_pDriver->GetDebugManager()->GetPipelineInfo(pipestate.graphics.pipeline);

      const ResourceId layoutID =
          (pipestate.graphics.shaderObject)
              ? pipestate.graphics.descSets[pipestate.graphics.LastBoundSet()].pipeLayout
              : p.vertLayout;

      const VulkanCreationInfo::PipelineLayout &layout =
          m_pDriver->GetDebugManager()->GetPipelineLayoutInfo(layoutID);

      const rdcarray<ResourceId> &origDescSetLayouts =
          (pipestate.graphics.shaderObject) ? layout.descSetLayouts : p.descSetLayouts;

      VkDescriptorSetLayout *descSetLayouts;

      // descSet will be the index of our new descriptor set
      uint32_t descSet = (uint32_t)origDescSetLayouts.size();

      descSetLayouts = new VkDescriptorSetLayout[descSet + 1];

      for(uint32_t i = 0; i < descSet; i++)
        descSetLayouts[i] =
            m_pDriver->GetResourceManager()->GetHandle<VkDescriptorSetLayout>(origDescSetLayouts[i]);

      // this layout has storage image
      descBuf = (p.flags & VK_PIPELINE_CREATE_DESCRIPTOR_BUFFER_BIT_EXT) != 0;

      if(descBuf)
        descSetLayouts[descSet] = m_DescBufLayout;
      else
        descSetLayouts[descSet] = m_DescSetLayout;

      // don't have to handle separate vert/frag layouts as push constant ranges must be identical
      const rdcarray<VkPushConstantRange> &push = layout.pushRanges;

      VkPipelineLayoutCreateInfo pipeLayoutInfo = {
          VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
          NULL,
          0,
          descSet + 1,
          descSetLayouts,
          (uint32_t)push.size(),
          push.empty() ? NULL : &push[0],
      };

      // create pipeline layout with same descriptor set layouts, plus our mesh output set
      if(pipestate.graphics.shaderObject)
        vkr = m_pDriver->vkCreatePipelineLayout(m_pDriver->GetDev(), &pipeLayoutInfo, NULL,
                                                &shad.pipeLayout);
      else
        vkr = m_pDriver->vkCreatePipelineLayout(m_pDriver->GetDev(), &pipeLayoutInfo, NULL,
                                                &pipe.pipeLayout);
      CHECK_VKR(m_pDriver, vkr);

      VkGraphicsPipelineCreateInfo pipeCreateInfo;
      m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                            pipestate.graphics.pipeline);

      if(!pipestate.graphics.shaderObject)
      {
        // repoint pipeline layout
        pipeCreateInfo.layout = pipe.pipeLayout;

        // disable colour writes/blends
        VkPipelineColorBlendStateCreateInfo *cb =
            (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState;
        for(uint32_t i = 0; i < cb->attachmentCount; i++)
        {
          VkPipelineColorBlendAttachmentState *att =
              (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
          att->blendEnable = false;
          att->colorWriteMask = 0x0;
        }

        // disable depth/stencil writes but keep any tests enabled
        VkPipelineDepthStencilStateCreateInfo *ds =
            (VkPipelineDepthStencilStateCreateInfo *)pipeCreateInfo.pDepthStencilState;
        ds->depthWriteEnable = false;
        ds->front.passOp = ds->front.failOp = ds->front.depthFailOp = VK_STENCIL_OP_KEEP;
        ds->back.passOp = ds->back.failOp = ds->back.depthFailOp = VK_STENCIL_OP_KEEP;

        // don't discard
        VkPipelineRasterizationStateCreateInfo *rs =
            (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState;
        rs->rasterizerDiscardEnable = false;
      }

      rdcarray<uint32_t> spirv = *m_pDriver->GetShaderCache()->GetBuiltinBlob(
          m_Multiview ? BuiltinShader::QuadWriteMultiviewFS : BuiltinShader::QuadWriteFS);

      // patch spirv, change descriptor set to descSet value
      size_t it = 5;
      while(it < spirv.size())
      {
        uint16_t WordCount = spirv[it] >> rdcspv::WordCountShift;
        rdcspv::Op opcode = rdcspv::Op(spirv[it] & rdcspv::OpCodeMask);

        if(opcode == rdcspv::Op::Decorate &&
           spirv[it + 2] == (uint32_t)rdcspv::Decoration::DescriptorSet)
        {
          spirv[it + 3] = descSet;
          break;
        }

        it += WordCount;
      }

      if(pipestate.graphics.shaderObject)
      {
        VkDevice dev = m_pDriver->GetDev();

        VkShaderCreateInfoEXT shadCreateInfo;
        m_pDriver->GetShaderCache()->MakeShaderObjectInfo(shadCreateInfo, pipestate.shaderObjects[4]);

        shadCreateInfo.pSetLayouts = descSetLayouts;
        shadCreateInfo.setLayoutCount = descSet + 1;
        shadCreateInfo.codeType = VK_SHADER_CODE_TYPE_SPIRV_EXT;
        shadCreateInfo.codeSize = spirv.size() * sizeof(uint32_t);
        shadCreateInfo.pCode = &spirv[0];
        shadCreateInfo.pName = "main";
        shadCreateInfo.pSpecializationInfo = NULL;

        vkr = m_pDriver->vkCreateShadersEXT(dev, 1, &shadCreateInfo, NULL, &shad.shad);
        CHECK_VKR(m_pDriver, vkr);

        shad.descSet = descSet;

        m_ShaderCache[pipestate.shaderObjects[4]] = shad;
      }
      else
      {
        VkShaderModuleCreateInfo modinfo = {
            VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            NULL,
            0,
            spirv.size() * sizeof(uint32_t),
            &spirv[0],
        };

        VkShaderModule module;

        VkDevice dev = m_pDriver->GetDev();

        vkr = m_pDriver->vkCreateShaderModule(dev, &modinfo, NULL, &module);
        CHECK_VKR(m_pDriver, vkr);

        bool found = false;
        for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++)
        {
          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i];
          if(sh.stage == VK_SHADER_STAGE_FRAGMENT_BIT)
          {
            sh.module = module;
            sh.pName = "main";
            found = true;
            break;
          }
        }

        if(!found)
        {
          // we know this is safe because it's pointing to a static array that's
          // big enough for all shaders

          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[pipeCreateInfo.stageCount++];
          sh.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
          sh.pNext = NULL;
          sh.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          sh.module = module;
          sh.pName = "main";
          sh.pSpecializationInfo = NULL;
        }

        vkr = m_pDriver->vkCreateGraphicsPipelines(dev, VK_NULL_HANDLE, 1, &pipeCreateInfo, NULL,
                                                   &pipe.pipe);
        CHECK_VKR(m_pDriver, vkr);

        m_pDriver->vkDestroyShaderModule(dev, module, NULL);

        pipe.descSet = descSet;

        m_PipelineCache[pipestate.graphics.pipeline] = pipe;
      }

      SAFE_DELETE_ARRAY(descSetLayouts);
    }

    // modify state for first draw call
    if(pipestate.graphics.shaderObject)
    {
      pipestate.shaderObjects[4] = GetResID(shad.shad);
      if(pipestate.graphics.UsingDescBufs())
        pipestate.graphics.lastBoundDescBufSet = shad.descSet;
      else
        pipestate.graphics.lastBoundDescSet = shad.descSet;
      pipestate.graphics.pipeline = ResourceId();

      RDCASSERT(pipestate.graphics.descSets.size() >= shad.descSet);
      pipestate.graphics.descSets.resize_for_index(shad.descSet);
      VulkanStatePipeline::DescriptorAndOffsets &descSet = pipestate.graphics.descSets[shad.descSet];

      descSet.pipeLayout = GetResID(shad.pipeLayout);
      if(descBuf)
      {
        descSet.descBufferEmbeddedSamplers = false;

        for(uint32_t i = 0; i < pipestate.descBufs.size(); i++)
        {
          if(pipestate.descBufs[i].usage & VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT)
          {
            descSet.descBufferIdx = i;
            ResourceId id;
            uint64_t ignored = 0;
            m_pDriver->GetResIDFromAddr(pipestate.descBufs[i].address, id, ignored);
            descSet.descBufferOffset = m_pDriver->GetDebugManager()->GetBufferInfo(id).size;
            break;
          }
        }
      }
      else
      {
        descSet.descSet = GetResID(m_DescSet);
      }
    }
    else
    {
      pipestate.graphics.pipeline = GetResID(pipe.pipe);

      RDCASSERT(pipestate.graphics.descSets.size() >= pipe.descSet);
      pipestate.graphics.descSets.resize_for_index(pipe.descSet);
      VulkanStatePipeline::DescriptorAndOffsets &descSet = pipestate.graphics.descSets[pipe.descSet];

      descSet.pipeLayout = GetResID(pipe.pipeLayout);
      if(descBuf)
      {
        descSet.descBufferEmbeddedSamplers = false;

        for(uint32_t i = 0; i < pipestate.descBufs.size(); i++)
        {
          if(pipestate.descBufs[i].usage & VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT)
          {
            descSet.descBufferIdx = i;
            ResourceId id;
            uint64_t ignored = 0;
            m_pDriver->GetResIDFromAddr(pipestate.descBufs[i].address, id, ignored);
            descSet.descBufferOffset = m_pDriver->GetDebugManager()->GetBufferInfo(id).size;
            break;
          }
        }
      }
      else
      {
        descSet.descSet = GetResID(m_DescSet);
      }
    }

    // modify dynamic state
    {
      // disable colour writes/blends
      for(uint32_t i = 0; i < pipestate.colorBlendEnable.size(); i++)
        pipestate.colorBlendEnable[i] = false;

      for(uint32_t i = 0; i < pipestate.colorWriteMask.size(); i++)
        pipestate.colorWriteMask[i] = 0x0;

      // disable depth/stencil writes but keep any tests enabled
      pipestate.depthWriteEnable = false;
      pipestate.front.passOp = pipestate.front.failOp = pipestate.front.depthFailOp =
          VK_STENCIL_OP_KEEP;
      pipestate.back.passOp = pipestate.back.failOp = pipestate.back.depthFailOp = VK_STENCIL_OP_KEEP;

      // don't discard
      pipestate.rastDiscardEnable = false;
    }

    if(cmd)
    {
      if(pipestate.graphics.shaderObject)
        pipestate.BindShaderObjects(m_pDriver, cmd, VulkanRenderState::BindGraphics);
      else
        pipestate.BindPipeline(m_pDriver, cmd, VulkanRenderState::BindGraphics, false);

      // Reset the attachment mapping, if any
      if(m_PrevState.dynamicRendering.localRead.AreLocationsNonDefault())
      {
        VkRenderingAttachmentLocationInfo attachmentLocations = {};
        attachmentLocations.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_LOCATION_INFO;
        m_pDriver->vkCmdSetRenderingAttachmentLocations(cmd, &attachmentLocations);
      }
      if(m_PrevState.dynamicRendering.localRead.AreInputIndicesNonDefault())
      {
        VkRenderingInputAttachmentIndexInfo inputIndices = {};
        inputIndices.sType = VK_STRUCTURE_TYPE_RENDERING_INPUT_ATTACHMENT_INDEX_INFO;

        m_pDriver->vkCmdSetRenderingInputAttachmentIndices(cmd, &inputIndices);
      }
    }
  }

  bool PostDraw(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd)
  {
    if(!m_Events.contains(eid))
      return false;

    // restore the render state and go ahead with the real draw
    m_pDriver->GetCmdRenderState() = m_PrevState;

    RDCASSERT(cmd);
    if(m_PrevState.graphics.shaderObject)
      m_pDriver->GetCmdRenderState().BindShaderObjects(m_pDriver, cmd,
                                                       VulkanRenderState::BindGraphics);
    else
      m_pDriver->GetCmdRenderState().BindPipeline(m_pDriver, cmd, VulkanRenderState::BindGraphics,
                                                  false);

    // Restore the attachment mappings, if any.
    if(m_PrevState.dynamicRendering.localRead.AreLocationsNonDefault())
    {
      m_PrevState.dynamicRendering.localRead.SetLocations(cmd);
    }

    if(m_PrevState.dynamicRendering.localRead.AreInputIndicesNonDefault())
    {
      m_PrevState.dynamicRendering.localRead.SetInputIndices(cmd);
    }

    return true;
  }

  void PostRedraw(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd)
  {
    // nothing to do
  }

  // Dispatches don't rasterize, so do nothing
  void PreDispatch(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) {}
  bool PostDispatch(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) { return false; }
  void PostRedispatch(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) {}
  // Ditto copy/etc
  void PreMisc(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) {}
  bool PostMisc(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) { return false; }
  void PostRemisc(uint32_t eid, ActionFlags flags, VkCommandBuffer cmd) {}
  void PreEndCommandBuffer(VkCommandBuffer cmd) {}
  void AliasEvent(uint32_t primary, uint32_t alias)
  {
    // don't care
  }
  bool SplitSecondary() { return false; }
  bool ForceLoadRPs() { return false; }
  void PreCmdExecute(uint32_t baseEid, uint32_t secondaryFirst, uint32_t secondaryLast,
                     VkCommandBuffer cmd)
  {
  }
  void PostCmdExecute(uint32_t baseEid, uint32_t secondaryFirst, uint32_t secondaryLast,
                      VkCommandBuffer cmd)
  {
  }

  WrappedVulkan *m_pDriver;
  VkDescriptorSetLayout m_DescSetLayout;
  VkDescriptorSet m_DescSet;
  VkDescriptorSetLayout m_DescBufLayout;
  const rdcarray<uint32_t> &m_Events;
  bool m_Multiview;

  // cache modified pipelines
  struct CachedPipeline
  {
    uint32_t descSet;
    VkPipelineLayout pipeLayout;
    VkPipeline pipe;
  };
  std::map<ResourceId, CachedPipeline> m_PipelineCache;
  // cache modified shader objects
  struct CachedShader
  {
    uint32_t descSet;
    VkPipelineLayout pipeLayout;
    VkShaderEXT shad;
  };
  std::map<ResourceId, CachedShader> m_ShaderCache;
  VulkanRenderState m_PrevState;
};

void VulkanDebugManager::PatchFixedColShader(VkShaderModule &mod, float col[4])
{
  union
  {
    uint32_t *spirv;
    float *data;
  } alias;

  rdcarray<uint32_t> spv = *m_pDriver->GetShaderCache()->GetBuiltinBlob(BuiltinShader::FixedColFS);

  alias.spirv = &spv[0];
  size_t spirvLength = spv.size();

  int patched = 0;

  size_t it = 5;
  while(it < spirvLength)
  {
    uint16_t WordCount = alias.spirv[it] >> rdcspv::WordCountShift;
    rdcspv::Op opcode = rdcspv::Op(alias.spirv[it] & rdcspv::OpCodeMask);

    if(opcode == rdcspv::Op::Constant)
    {
      if(alias.data[it + 3] >= 1.0f && alias.data[it + 3] <= 1.5f)
        alias.data[it + 3] = col[0];
      else if(alias.data[it + 3] >= 2.0f && alias.data[it + 3] <= 2.5f)
        alias.data[it + 3] = col[1];
      else if(alias.data[it + 3] >= 3.0f && alias.data[it + 3] <= 3.5f)
        alias.data[it + 3] = col[2];
      else if(alias.data[it + 3] >= 4.0f && alias.data[it + 3] <= 4.5f)
        alias.data[it + 3] = col[3];
      else
        RDCERR("Unexpected constant value");

      patched++;
    }

    it += WordCount;
  }

  if(patched != 4)
    RDCERR("Didn't patch all constants");

  VkShaderModuleCreateInfo modinfo = {
      VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
      NULL,
      0,
      spv.size() * sizeof(uint32_t),
      alias.spirv,
  };

  VkResult vkr = m_pDriver->vkCreateShaderModule(m_Device, &modinfo, NULL, &mod);
  CHECK_VKR(m_pDriver, vkr);
}

void VulkanDebugManager::PatchFixedColShaderObject(VkShaderEXT &shad, float col[4])
{
  union
  {
    uint32_t *spirv;
    float *data;
  } alias;

  rdcarray<uint32_t> spv = *m_pDriver->GetShaderCache()->GetBuiltinBlob(BuiltinShader::FixedColFS);

  alias.spirv = &spv[0];
  size_t spirvLength = spv.size();

  int patched = 0;

  size_t it = 5;
  while(it < spirvLength)
  {
    uint16_t WordCount = alias.spirv[it] >> rdcspv::WordCountShift;
    rdcspv::Op opcode = rdcspv::Op(alias.spirv[it] & rdcspv::OpCodeMask);

    if(opcode == rdcspv::Op::Constant)
    {
      if(alias.data[it + 3] >= 1.0f && alias.data[it + 3] <= 1.5f)
        alias.data[it + 3] = col[0];
      else if(alias.data[it + 3] >= 2.0f && alias.data[it + 3] <= 2.5f)
        alias.data[it + 3] = col[1];
      else if(alias.data[it + 3] >= 3.0f && alias.data[it + 3] <= 3.5f)
        alias.data[it + 3] = col[2];
      else if(alias.data[it + 3] >= 4.0f && alias.data[it + 3] <= 4.5f)
        alias.data[it + 3] = col[3];
      else
        RDCERR("Unexpected constant value");

      patched++;
    }

    it += WordCount;
  }

  if(patched != 4)
    RDCERR("Didn't patch all constants");

  VkShaderCreateInfoEXT shadInfo = {VK_STRUCTURE_TYPE_SHADER_CREATE_INFO_EXT,
                                    NULL,
                                    0,
                                    VK_SHADER_STAGE_FRAGMENT_BIT,
                                    0,
                                    VK_SHADER_CODE_TYPE_SPIRV_EXT,
                                    spv.size() * sizeof(uint32_t),
                                    alias.spirv,
                                    "main",
                                    0,
                                    NULL,
                                    0,
                                    NULL,
                                    NULL};

  VkResult vkr = m_pDriver->vkCreateShadersEXT(m_Device, 1, &shadInfo, NULL, &shad);

  CHECK_VKR(m_pDriver, vkr);
}

void VulkanDebugManager::PatchLineStripIndexBuffer(const ActionDescription *action,
                                                   GPUBuffer &indexBuffer, uint32_t &indexCount)
{
  VulkanRenderState &rs = m_pDriver->m_RenderState;

  bytebuf indices;

  uint8_t *idx8 = NULL;
  uint16_t *idx16 = NULL;
  uint32_t *idx32 = NULL;

  if(action->flags & ActionFlags::Indexed)
  {
    uint64_t readSizeBytes = uint64_t(action->numIndices) * rs.ibuffer.bytewidth;
    // clamp to handle subrange bound via vkCmdBindIndexBuffer2
    if(rs.ibuffer.size != VK_WHOLE_SIZE)
    {
      uint64_t offsetBytes = uint64_t(action->indexOffset) * rs.ibuffer.bytewidth;
      uint64_t maxSubrangeBytes = rs.ibuffer.size > offsetBytes ? rs.ibuffer.size - offsetBytes : 0;

      readSizeBytes = RDCMIN(readSizeBytes, maxSubrangeBytes);
    }

    if(rs.ibuffer.buf == ResourceId())
      indices.resize((size_t)readSizeBytes);
    else
      GetBufferData(rs.ibuffer.buf,
                    rs.ibuffer.offs + uint64_t(action->indexOffset) * rs.ibuffer.bytewidth,
                    readSizeBytes, indices);

    if(rs.ibuffer.bytewidth == 4)
      idx32 = (uint32_t *)indices.data();
    else if(rs.ibuffer.bytewidth == 1)
      idx8 = (uint8_t *)indices.data();
    else
      idx16 = (uint16_t *)indices.data();
  }

  // we just patch up to 32-bit since we'll be adding more indices and we might overflow 16-bit.
  rdcarray<uint32_t> patchedIndices;

  ::PatchLineStripIndexBuffer(action, MakePrimitiveTopology(rs.primitiveTopology, 3), idx8, idx16,
                              idx32, patchedIndices);

  indexBuffer.Create(m_pDriver, m_Device, patchedIndices.size() * sizeof(uint32_t), 1,
                     GPUBuffer::eGPUBufferIBuffer);
  indexBuffer.Name("PatchedStripIB");

  void *ptr = indexBuffer.Map(0, patchedIndices.size() * sizeof(uint32_t));
  if(!ptr)
    return;
  memcpy(ptr, patchedIndices.data(), patchedIndices.size() * sizeof(uint32_t));
  indexBuffer.Unmap();

  VkBufferMemoryBarrier uploadbarrier = {
      VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
      NULL,
      VK_ACCESS_HOST_WRITE_BIT,
      VK_ACCESS_INDEX_READ_BIT,
      VK_QUEUE_FAMILY_IGNORED,
      VK_QUEUE_FAMILY_IGNORED,
      indexBuffer.UnwrappedBuffer(),
      0,
      indexBuffer.TotalSize(),
  };

  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 = ObjDisp(m_Device)->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
  CHECK_VKR(m_pDriver, vkr);

  // ensure host writes finish before using as index buffer
  DoPipelineBarrier(cmd, 1, &uploadbarrier);

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

  indexCount = (uint32_t)patchedIndices.size();
}

RenderOutputSubresource VulkanReplay::GetRenderOutputSubresource(ResourceId id)
{
  const VulkanRenderState &state = m_pDriver->m_RenderState;
  VulkanCreationInfo &c = m_pDriver->m_CreationInfo;

  for(ResourceId viewid : state.GetFramebufferAttachments())
  {
    const VulkanCreationInfo::ImageView &viewInfo = c.m_ImageView[viewid];

    if(viewid == id || viewInfo.image == id)
    {
      return RenderOutputSubresource(viewInfo.range.baseMipLevel,
                                     c.m_ImageView[viewid].range.baseArrayLayer,
                                     c.m_ImageView[viewid].range.layerCount);
    }
  }

  return RenderOutputSubresource(~0U, ~0U, 0);
}

ResourceId VulkanReplay::RenderOverlay(ResourceId texid, FloatVector clearCol, DebugOverlay overlay,
                                       uint32_t eventId, const rdcarray<uint32_t> &passEvents)
{
  const VkDevDispatchTable *vt = ObjDisp(m_Device);

  RenderOutputSubresource sub = GetRenderOutputSubresource(texid);

  if(sub.slice == ~0U)
  {
    RDCERR("Rendering overlay for %s couldn't find output to get subresource.", ToStr(texid).c_str());
    sub = RenderOutputSubresource(0, 0, 1);
  }

  VulkanShaderCache *shaderCache = m_pDriver->GetShaderCache();

  VulkanCreationInfo::Image &iminfo = m_pDriver->m_CreationInfo.m_Image[texid];

  VkCommandBuffer cmd = m_pDriver->GetNextCmd();

  if(cmd == VK_NULL_HANDLE)
    return ResourceId();

  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);
  CHECK_VKR(m_pDriver, vkr);

  VkMarkerRegion::Begin(StringFormat::Fmt("RenderOverlay %d", overlay), cmd);

  uint32_t multiviewMask = m_Overlay.MultiViewMask;

  VulkanRenderState &state = m_pDriver->m_RenderState;
  if(state.dynamicRendering.active)
  {
    multiviewMask = state.dynamicRendering.viewMask;
  }
  else if(state.GetRenderPass() != ResourceId())
  {
    const VulkanCreationInfo::RenderPass &rp =
        m_pDriver->m_CreationInfo.m_RenderPass[state.GetRenderPass()];

    multiviewMask = 0;
    for(uint32_t v : rp.subpasses[state.subpass].multiviews)
      multiviewMask |= 1U << v;
  }

  // if the overlay image is the wrong size, free it
  if(m_Overlay.Image != VK_NULL_HANDLE &&
     (iminfo.extent.width != m_Overlay.ImageDim.width ||
      iminfo.extent.height != m_Overlay.ImageDim.height || iminfo.samples != m_Overlay.Samples ||
      iminfo.mipLevels != m_Overlay.MipLevels || iminfo.arrayLayers != m_Overlay.ArrayLayers ||
      multiviewMask != m_Overlay.MultiViewMask))
  {
    m_pDriver->vkDestroyRenderPass(m_Device, m_Overlay.NoDepthRP, NULL);
    m_pDriver->vkDestroyFramebuffer(m_Device, m_Overlay.NoDepthFB, NULL);
    m_pDriver->vkDestroyImageView(m_Device, m_Overlay.ImageView, NULL);
    m_pDriver->vkDestroyImage(m_Device, m_Overlay.Image, NULL);

    m_Overlay.Image = VK_NULL_HANDLE;
    m_Overlay.ImageView = VK_NULL_HANDLE;
    m_Overlay.NoDepthRP = VK_NULL_HANDLE;
    m_Overlay.NoDepthFB = VK_NULL_HANDLE;
  }

  VkImageSubresourceRange subRange = {VK_IMAGE_ASPECT_COLOR_BIT, sub.mip, 1, sub.slice,
                                      sub.numSlices};

  const VkFormat overlayFormat = VK_FORMAT_R16G16B16A16_SFLOAT;

  VkRenderPassMultiviewCreateInfo multiviewRP = {VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO};
  multiviewRP.correlationMaskCount = 1;
  multiviewRP.pCorrelationMasks = &multiviewMask;
  multiviewRP.subpassCount = 1;
  multiviewRP.pViewMasks = &multiviewMask;

  // create the overlay image if we don't have one already
  // we go through the driver's creation functions so creation info
  // is saved and the resources are registered as live resources for
  // their IDs.
  if(m_Overlay.Image == VK_NULL_HANDLE)
  {
    m_Overlay.ImageDim.width = iminfo.extent.width;
    m_Overlay.ImageDim.height = iminfo.extent.height;
    m_Overlay.MipLevels = iminfo.mipLevels;
    m_Overlay.ArrayLayers = iminfo.arrayLayers;
    m_Overlay.Samples = iminfo.samples;
    m_Overlay.MultiViewMask = multiviewMask;

    VkImageCreateInfo imInfo = {
        VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
        NULL,
        0,
        VK_IMAGE_TYPE_2D,
        overlayFormat,
        {m_Overlay.ImageDim.width, m_Overlay.ImageDim.height, 1},
        (uint32_t)iminfo.mipLevels,
        (uint32_t)iminfo.arrayLayers,
        iminfo.samples,
        VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
            VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
        VK_SHARING_MODE_EXCLUSIVE,
        0,
        NULL,
        VK_IMAGE_LAYOUT_UNDEFINED,
    };

    vkr = m_pDriver->vkCreateImage(m_Device, &imInfo, NULL, &m_Overlay.Image);
    CHECK_VKR(m_pDriver, vkr);

    NameVulkanObject(m_Overlay.Image, "m_Overlay.Image");

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

    // if no memory is allocated, or it's not enough,
    // then allocate
    if(m_Overlay.ImageMem == VK_NULL_HANDLE || mrq.size > m_Overlay.ImageMemSize)
    {
      if(m_Overlay.ImageMem != VK_NULL_HANDLE)
      {
        m_pDriver->vkFreeMemory(m_Device, m_Overlay.ImageMem, 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_Overlay.ImageMem);
      CHECK_VKR(m_pDriver, vkr);

      if(vkr != VK_SUCCESS)
        return ResourceId();

      m_Overlay.ImageMemSize = mrq.size;
    }

    vkr = m_pDriver->vkBindImageMemory(m_Device, m_Overlay.Image, m_Overlay.ImageMem, 0);
    CHECK_VKR(m_pDriver, vkr);

    // need to update image layout into valid state

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

    VkAttachmentDescription colDesc = {
        0,
        imInfo.format,
        imInfo.samples,
        VK_ATTACHMENT_LOAD_OP_LOAD,
        VK_ATTACHMENT_STORE_OP_STORE,
        VK_ATTACHMENT_LOAD_OP_DONT_CARE,
        VK_ATTACHMENT_STORE_OP_DONT_CARE,
        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    };

    VkAttachmentReference colRef = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};

    VkSubpassDescription subp = {
        0,    VK_PIPELINE_BIND_POINT_GRAPHICS,
        0,    NULL,       // inputs
        1,    &colRef,    // color
        NULL,             // resolve
        NULL,             // depth-stencil
        0,    NULL,       // preserve
    };

    VkRenderPassCreateInfo rpinfo = {
        VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
        NULL,
        0,
        1,
        &colDesc,
        1,
        &subp,
        0,
        NULL,    // dependencies
    };

    if(multiviewMask > 0)
      rpinfo.pNext = &multiviewRP;

    vkr = m_pDriver->vkCreateRenderPass(m_Device, &rpinfo, NULL, &m_Overlay.NoDepthRP);
    CHECK_VKR(m_pDriver, vkr);
  }

  if(m_Overlay.ViewMip != sub.mip || m_Overlay.ViewSlice != sub.slice ||
     m_Overlay.ViewNumSlices != sub.numSlices || m_Overlay.ImageView == VK_NULL_HANDLE)
  {
    m_pDriver->vkDestroyFramebuffer(m_Device, m_Overlay.NoDepthFB, NULL);
    m_pDriver->vkDestroyImageView(m_Device, m_Overlay.ImageView, NULL);

    m_Overlay.ViewMip = sub.mip;
    m_Overlay.ViewSlice = sub.slice;
    m_Overlay.ViewNumSlices = sub.numSlices;

    VkImageViewCreateInfo viewInfo = {
        VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        NULL,
        0,
        m_Overlay.Image,
        VK_IMAGE_VIEW_TYPE_2D,
        overlayFormat,
        {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
         VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
        subRange,
    };

    if(subRange.layerCount > 1)
      viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;

    vkr = m_pDriver->vkCreateImageView(m_Device, &viewInfo, NULL, &m_Overlay.ImageView);
    CHECK_VKR(m_pDriver, vkr);

    // Create framebuffer rendering just to overlay image, no depth
    VkFramebufferCreateInfo fbinfo = {
        VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
        NULL,
        0,
        m_Overlay.NoDepthRP,
        1,
        &m_Overlay.ImageView,
        RDCMAX(1U, m_Overlay.ImageDim.width >> sub.mip),
        RDCMAX(1U, m_Overlay.ImageDim.height >> sub.mip),
        sub.numSlices,
    };

    if(multiviewMask > 0)
      fbinfo.layers = 1;

    vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &m_Overlay.NoDepthFB);
    CHECK_VKR(m_pDriver, vkr);

    // can't create a framebuffer or renderpass for overlay image + depth as that
    // needs to match the depth texture type wherever our draw is.
  }

  // bail out if the render area is outside our image.
  // This is an order-of-operations problem, if the overlay is set when the event is changed it is
  // refreshed before the UI layer can update the current texture.
  if(state.renderArea.offset.x + state.renderArea.extent.width >
         (m_Overlay.ImageDim.width >> sub.mip) ||
     state.renderArea.offset.y + state.renderArea.extent.height >
         (m_Overlay.ImageDim.height >> sub.mip))
  {
    return GetResID(m_Overlay.Image);
  }

  {
    VkImageSubresourceRange fullSubRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS,
                                            0, VK_REMAINING_ARRAY_LAYERS};

    VkImageMemoryBarrier barrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        NULL,
        VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        VK_IMAGE_LAYOUT_UNDEFINED,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        Unwrap(m_Overlay.Image),
        fullSubRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    float black[4] = {};
    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)black, 1, &fullSubRange);

    barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask =
        VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);
  }

  const ActionDescription *mainDraw = m_pDriver->GetAction(eventId);

  const VulkanCreationInfo::Pipeline &pipeInfo =
      m_pDriver->m_CreationInfo.m_Pipeline[state.graphics.pipeline];

  bool viewportPerView = false;

  // if we have multiview and _exactly_ as many viewports as multiview, assume a sane 1:1
  // mapping in reality this depends on shader execution to export ViewportIndex
  // if multiview is disabled we just use the first viewport/scissor
  if(multiviewMask == (1U << (state.views.size())) - 1)
    viewportPerView = true;

  // if the extension is enabled to auto-select viewport per-view, check if the shader doesn't select
  if(m_pDriver->MultiviewPerViewViewports())
  {
    if(!state.graphics.shaderObject)
    {
      uint32_t shadIdx = (uint32_t)ShaderStage::Vertex;
      if(pipeInfo.shaders[(uint32_t)ShaderStage::Geometry].module != ResourceId())
        shadIdx = (uint32_t)ShaderStage::Geometry;
      else if(pipeInfo.shaders[(uint32_t)ShaderStage::Tess_Eval].module != ResourceId())
        shadIdx = (uint32_t)ShaderStage::Tess_Eval;

      const ShaderReflection *reflection = pipeInfo.shaders[shadIdx].refl;
      bool outputsViewport = false;
      for(const SigParameter &output : reflection->outputSignature)
      {
        if(output.systemValue == ShaderBuiltin::ViewportIndex)
          outputsViewport = true;
      }

      if(!outputsViewport)
        viewportPerView = true;
    }
    else
    {
      VulkanCreationInfo &createinfo = m_pDriver->m_CreationInfo;

      uint32_t shadIdx = (uint32_t)ShaderStage::Vertex;
      if(state.shaderObjects[(uint32_t)ShaderStage::Geometry] != ResourceId())
        shadIdx = (uint32_t)ShaderStage::Geometry;
      else if(state.shaderObjects[(uint32_t)ShaderStage::Tess_Eval] != ResourceId())
        shadIdx = (uint32_t)ShaderStage::Tess_Eval;

      const ShaderReflection *reflection =
          createinfo.m_ShaderObject[state.shaderObjects[shadIdx]].shad.refl;
      bool outputsViewport = false;
      for(const SigParameter &output : reflection->outputSignature)
      {
        if(output.systemValue == ShaderBuiltin::ViewportIndex)
          outputsViewport = true;
      }

      if(!outputsViewport)
        viewportPerView = true;
    }
  }

  bool rpActive = m_pDriver->IsPartialRenderPassActiveUnsuspended();

  if((mainDraw && !(mainDraw->flags & (ActionFlags::MeshDispatch | ActionFlags::Drawcall))) ||
     !rpActive)
  {
    // don't do anything, no action capable of making overlays selected
    float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)black, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);
  }
  else if(overlay == DebugOverlay::NaN || overlay == DebugOverlay::Clipping)
  {
    float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)black, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);
  }
  else if(overlay == DebugOverlay::Drawcall || overlay == DebugOverlay::Wireframe)
  {
    float highlightCol[] = {0.8f, 0.1f, 0.8f, 1.0f};
    float bgclearCol[] = {0.0f, 0.0f, 0.0f, 0.5f};

    if(overlay == DebugOverlay::Wireframe)
    {
      highlightCol[0] = 200 / 255.0f;
      highlightCol[1] = 1.0f;
      highlightCol[2] = 0.0f;

      bgclearCol[0] = 200 / 255.0f;
      bgclearCol[1] = 1.0f;
      bgclearCol[2] = 0.0f;
      bgclearCol[3] = 0.0f;
    }

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)bgclearCol, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);

    if(!state.rastDiscardEnable)
    {
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      // backup state
      VulkanRenderState prevstate = state;

      // make patched shader
      VkShaderModule mod = VK_NULL_HANDLE;

      GetDebugManager()->PatchFixedColShader(mod, highlightCol);

      // make patched pipeline
      VkGraphicsPipelineCreateInfo pipeCreateInfo;

      m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                            prevstate.graphics.pipeline);

      // make patched shader object
      VkShaderEXT shad = VK_NULL_HANDLE;

      if(state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShaderObject(shad, highlightCol);

      // disable all tests possible
      VkPipelineDepthStencilStateCreateInfo *ds =
          (VkPipelineDepthStencilStateCreateInfo *)pipeCreateInfo.pDepthStencilState;
      if(ds)
      {
        ds->depthTestEnable = false;
        ds->depthWriteEnable = false;
        ds->stencilTestEnable = false;
        ds->depthBoundsTestEnable = false;
      }

      VkPipelineRasterizationStateCreateInfo *rs =
          (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState;
      if(rs)
      {
        rs->cullMode = VK_CULL_MODE_NONE;
        rs->rasterizerDiscardEnable = false;
      }

      VkPipelineMultisampleStateCreateInfo *msaa =
          (VkPipelineMultisampleStateCreateInfo *)pipeCreateInfo.pMultisampleState;
      if(msaa)
        msaa->pSampleMask = NULL;

      // disable tests in dynamic state too
      state.depthTestEnable = VK_FALSE;
      state.depthWriteEnable = VK_FALSE;
      state.depthCompareOp = VK_COMPARE_OP_ALWAYS;
      state.stencilTestEnable = VK_FALSE;
      state.depthBoundsTestEnable = VK_FALSE;
      state.cullMode = VK_CULL_MODE_NONE;

      state.sampleMask = {~0U};

      // disable all discard rectangles
      RemoveNextStruct(&pipeCreateInfo,
                       VK_STRUCTURE_TYPE_PIPELINE_DISCARD_RECTANGLE_STATE_CREATE_INFO_EXT);

      if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
      {
        if(rs)
          rs->depthClampEnable = true;
        state.depthClampEnable = true;
      }

      // disable line stipple
      VkPipelineRasterizationLineStateCreateInfo *lineRasterState =
          (VkPipelineRasterizationLineStateCreateInfo *)FindNextStruct(
              rs, VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_LINE_STATE_CREATE_INFO);

      if(lineRasterState)
      {
        lineRasterState->stippledLineEnable = VK_FALSE;
      }
      state.stippledLineEnable = false;

      uint32_t patchedIndexCount = 0;
      GPUBuffer patchedIB;

      if(overlay == DebugOverlay::Wireframe)
      {
        if(rs)
          rs->lineWidth = 1.0f;

        if(mainDraw == NULL)
        {
          // do nothing
        }
        else if(m_pDriver->GetDeviceEnabledFeatures().fillModeNonSolid)
        {
          if(rs)
            rs->polygonMode = VK_POLYGON_MODE_LINE;
          state.polygonMode = VK_POLYGON_MODE_LINE;

          if(m_pDriver->ShaderObject())
            state.dynamicStates[VkDynamicLineWidth] = true;
        }
        else if(prevstate.primitiveTopology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST ||
                prevstate.primitiveTopology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP ||
                prevstate.primitiveTopology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN ||
                prevstate.primitiveTopology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY ||
                prevstate.primitiveTopology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY)
        {
          // bad drivers (aka mobile) won't have non-solid fill mode, so we have to fall back to
          // manually patching the index buffer and using a line list. This doesn't work with
          // adjacency or patchlist topologies since those imply a vertex processing pipeline that
          // requires a particular topology, or can't be implicitly converted to lines at input
          // stage.
          // It's unlikely those features will be used on said poor hw, so this should still catch
          // most cases.
          VkPipelineInputAssemblyStateCreateInfo *ia =
              (VkPipelineInputAssemblyStateCreateInfo *)pipeCreateInfo.pInputAssemblyState;

          if(ia)
            ia->topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;

          // thankfully, primitive restart is always supported! This makes the index buffer a bit
          // more
          // compact in the common cases where we don't need to repeat two indices for a triangle's
          // three lines, instead we have a single restart index after each triangle.
          if(ia)
            ia->primitiveRestartEnable = true;

          GetDebugManager()->PatchLineStripIndexBuffer(mainDraw, patchedIB, patchedIndexCount);

          state.primitiveTopology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
          state.primRestartEnable = true;

          if(m_pDriver->ShaderObject())
            state.dynamicStates[VkDynamicLineWidth] = true;
        }
        else
        {
          RDCWARN("Unable to draw wireframe overlay for %s topology draw via software patching",
                  ToStr(prevstate.primitiveTopology).c_str());
        }
      }

      VkPipelineColorBlendStateCreateInfo *cb =
          (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState;
      if(cb)
      {
        cb->logicOpEnable = false;
        cb->attachmentCount = 1;    // only one colour attachment
        for(uint32_t i = 0; i < cb->attachmentCount; i++)
        {
          VkPipelineColorBlendAttachmentState *att =
              (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
          att->blendEnable = false;
          att->colorWriteMask = 0xf;
        }
      }

      state.logicOpEnable = false;
      for(uint32_t i = 0; i < state.colorBlendEnable.size(); i++)
        state.colorBlendEnable[i] = false;
      for(uint32_t i = 0; i < state.colorWriteMask.size(); i++)
        state.colorWriteMask[i] = 0xf;

      // set scissors to max for drawcall
      if(overlay == DebugOverlay::Drawcall && pipeCreateInfo.pViewportState)
      {
        for(size_t i = 0; i < pipeCreateInfo.pViewportState->scissorCount; i++)
        {
          VkRect2D &sc = (VkRect2D &)pipeCreateInfo.pViewportState->pScissors[i];
          sc.offset.x = 0;
          sc.offset.y = 0;
          sc.extent.width = 16384;
          sc.extent.height = 16384;
        }
      }

      // set our renderpass and shader
      pipeCreateInfo.renderPass = m_Overlay.NoDepthRP;
      pipeCreateInfo.subpass = 0;

      // don't use dynamic rendering
      RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO);
      // don't use custom resolve
      RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_CUSTOM_RESOLVE_CREATE_INFO_EXT);

      if(!state.graphics.shaderObject)
      {
        bool found = false;
        for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++)
        {
          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i];
          if(sh.stage == VK_SHADER_STAGE_FRAGMENT_BIT)
          {
            sh.module = mod;
            sh.pName = "main";
            found = true;
            break;
          }
        }

        if(!found)
        {
          // we know this is safe because it's pointing to a static array that's
          // big enough for all shaders

          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[pipeCreateInfo.stageCount++];
          sh.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
          sh.pNext = NULL;
          sh.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          sh.module = mod;
          sh.pName = "main";
          sh.pSpecializationInfo = NULL;
        }
      }

      VkPipeline pipe = VK_NULL_HANDLE;

      if(!state.graphics.shaderObject)
      {
        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &pipe);
        CHECK_VKR(m_pDriver, vkr);
      }

      // modify state
      state.SetRenderPass(GetResID(m_Overlay.NoDepthRP));
      state.subpass = 0;
      state.SetFramebuffer(m_pDriver, GetResID(m_Overlay.NoDepthFB));
      state.dynamicRendering.beginCustomResolve = false;

      state.subpassContents = VK_SUBPASS_CONTENTS_INLINE;
      state.dynamicRendering.flags &= ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT;

      state.graphics.pipeline = GetResID(pipe);

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(shad);
      }

      // set dynamic scissors in case pipeline was using them
      if(overlay == DebugOverlay::Drawcall)
      {
        for(size_t i = 0; i < state.scissors.size(); i++)
        {
          state.scissors[i].offset.x = 0;
          state.scissors[i].offset.y = 0;
          state.scissors[i].extent.width = 16384;
          state.scissors[i].extent.height = 16384;
        }
      }

      if(overlay == DebugOverlay::Wireframe)
        state.lineWidth = 1.0f;

      if(overlay == DebugOverlay::Drawcall || overlay == DebugOverlay::Wireframe)
        state.conditionalRendering.forceDisable = true;

      if(patchedIndexCount == 0)
      {
        m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);
      }
      else
      {
        // if we patched the index buffer we need to manually play the draw with a higher index
        // count
        // and no index offset.
        cmd = m_pDriver->GetNextCmd();

        if(cmd == VK_NULL_HANDLE)
          return ResourceId();

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

        // do single draw
        state.BeginRenderPassAndApplyState(m_pDriver, cmd, VulkanRenderState::BindGraphics, false);

        ObjDisp(cmd)->CmdBindIndexBuffer(Unwrap(cmd), patchedIB.UnwrappedBuffer(), 0,
                                         VK_INDEX_TYPE_UINT32);

        ActionDescription action = *mainDraw;
        action.numIndices = patchedIndexCount;
        action.baseVertex = 0;
        action.indexOffset = 0;
        action.flags |= ActionFlags::Indexed;
        m_pDriver->ReplayDraw(cmd, action);
        state.EndRenderPass(cmd);

        vkr = ObjDisp(cmd)->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);
      }

      // submit & flush so that we don't have to keep pipeline around for a while
      m_pDriver->SubmitCmds();
      m_pDriver->FlushQ();

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

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

      // restore state
      state = prevstate;

      patchedIB.Destroy();

      if(shad != VK_NULL_HANDLE)
        m_pDriver->vkDestroyShaderEXT(m_Device, shad, NULL);

      m_pDriver->vkDestroyPipeline(m_Device, pipe, NULL);
      m_pDriver->vkDestroyShaderModule(m_Device, mod, NULL);
    }
  }
  else if(overlay == DebugOverlay::ViewportScissor)
  {
    // clear the whole image to opaque black. We'll overwite the render area with transparent black
    // before rendering the viewport/scissors
    float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)black, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);

    black[3] = 0.0f;

    if(!state.rastDiscardEnable)
    {
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      float highlightCol[] = {1.0f, 0.0f, 0.0f, 1.0f};

      // backup state
      VulkanRenderState prevstate = state;

      // make patched shader
      VkPipeline checkerPipe = VK_NULL_HANDLE;
      VkShaderModule mod[2] = {0};
      VkPipeline pipe[2] = {0};
      VkShaderEXT shad[2] = {0};

      // first shader, no culling, writes red
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(mod[0], highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(shad[0], highlightCol);

      highlightCol[0] = 0.0f;
      highlightCol[1] = 1.0f;

      // second shader, normal culling, writes green
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(mod[1], highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(shad[1], highlightCol);

      // make patched pipeline
      VkGraphicsPipelineCreateInfo pipeCreateInfo;

      m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                            prevstate.graphics.pipeline);

      if(!state.graphics.shaderObject)
      {
        // disable all tests possible
        VkPipelineDepthStencilStateCreateInfo *ds =
            (VkPipelineDepthStencilStateCreateInfo *)pipeCreateInfo.pDepthStencilState;
        ds->depthTestEnable = false;
        ds->depthWriteEnable = false;
        ds->stencilTestEnable = false;
        ds->depthBoundsTestEnable = false;

        VkPipelineRasterizationStateCreateInfo *rs =
            (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState;
        rs->cullMode = VK_CULL_MODE_NONE;    // first render without any culling
        rs->rasterizerDiscardEnable = false;

        VkPipelineMultisampleStateCreateInfo *msaa =
            (VkPipelineMultisampleStateCreateInfo *)pipeCreateInfo.pMultisampleState;
        msaa->pSampleMask = NULL;

        if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
          rs->depthClampEnable = true;

        VkPipelineColorBlendStateCreateInfo *cb =
            (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState;
        cb->logicOpEnable = false;
        cb->attachmentCount = 1;    // only one colour attachment
        for(uint32_t i = 0; i < cb->attachmentCount; i++)
        {
          VkPipelineColorBlendAttachmentState *att =
              (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
          att->blendEnable = false;
          att->colorWriteMask = 0xf;
        }

        // set our renderpass and shader
        pipeCreateInfo.renderPass = m_Overlay.NoDepthRP;
        pipeCreateInfo.subpass = 0;

        // don't use dynamic rendering
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO);
        // don't use custom resolve
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_CUSTOM_RESOLVE_CREATE_INFO_EXT);

        VkPipelineShaderStageCreateInfo *fragShader = NULL;
        for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++)
        {
          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i];
          if(sh.stage == VK_SHADER_STAGE_FRAGMENT_BIT)
          {
            sh.module = mod[0];
            sh.pName = "main";
            fragShader = &sh;
            break;
          }
        }

        if(fragShader == NULL)
        {
          // we know this is safe because it's pointing to a static array that's
          // big enough for all shaders

          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[pipeCreateInfo.stageCount++];
          sh.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
          sh.pNext = NULL;
          sh.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          sh.module = mod[0];
          sh.pName = "main";
          sh.pSpecializationInfo = NULL;

          fragShader = &sh;
        }

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &pipe[0]);
        CHECK_VKR(m_pDriver, vkr);

        fragShader->module = mod[1];

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &pipe[1]);
        CHECK_VKR(m_pDriver, vkr);
      }

      // disable tests in dynamic state too
      state.depthTestEnable = VK_FALSE;
      state.depthWriteEnable = VK_FALSE;
      state.depthCompareOp = VK_COMPARE_OP_ALWAYS;
      state.stencilTestEnable = VK_FALSE;
      state.depthBoundsTestEnable = VK_FALSE;
      state.cullMode = VK_CULL_MODE_NONE;

      state.sampleMask = {~0U};

      // enable dynamic depth clamp
      if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
        state.depthClampEnable = true;

      state.logicOpEnable = false;
      for(uint32_t i = 0; i < state.colorBlendEnable.size(); i++)
        state.colorBlendEnable[i] = false;
      for(uint32_t i = 0; i < state.colorWriteMask.size(); i++)
        state.colorWriteMask[i] = 0xf;

      // modify state
      state.SetRenderPass(GetResID(m_Overlay.NoDepthRP));
      state.subpass = 0;
      state.SetFramebuffer(m_pDriver, GetResID(m_Overlay.NoDepthFB));
      state.dynamicRendering.beginCustomResolve = false;

      state.graphics.pipeline = GetResID(pipe[0]);
      state.scissors = prevstate.scissors;

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(shad[0]);
      }

      for(VkRect2D &sc : state.scissors)
      {
        sc.offset.x = 0;
        sc.offset.y = 0;
        sc.extent.width = 16384;
        sc.extent.height = 16384;
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      state.graphics.pipeline = GetResID(pipe[1]);
      state.scissors = prevstate.scissors;

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(shad[1]);
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      // restore state
      state = prevstate;

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

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

      uint32_t firstView = 0;
      uint32_t lastView = 0;

      if(viewportPerView)
      {
        firstView = Bits::CountTrailingZeroes(multiviewMask);
        lastView = 31 - Bits::CountLeadingZeroes(multiviewMask);
      }

      VkClearValue clearval = {};
      VkRenderPassBeginInfo rpbegin = {
          VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
          NULL,
          Unwrap(m_Overlay.NoDepthRP),
          Unwrap(m_Overlay.NoDepthFB),
          state.renderArea,
          1,
          &clearval,
      };
      vt->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

      checkerPipe = m_Overlay.CreateTempViewportPipe(m_pDriver, lastView - firstView + 1);

      for(uint32_t viewIndex = firstView; viewIndex <= lastView; viewIndex++)
      {
        VkViewport viewport = state.views[viewIndex];
        // set all viewports identically in case we're using the implicit-viewport extension
        rdcarray<VkViewport> allviews;
        allviews.fill(state.views.count(), viewport);
        vt->CmdSetViewport(Unwrap(cmd), 0, allviews.count(), allviews.data());

        uint32_t uboOffs = 0;

        CheckerboardUBOData *ubo = (CheckerboardUBOData *)m_Overlay.m_CheckerUBO.Map(&uboOffs);
        if(!ubo)
          return ResourceId();

        ubo->BorderWidth = 3;
        ubo->CheckerSquareDimension = 16.0f;

        // set primary/secondary to the same to 'disable' checkerboard
        ubo->PrimaryColor = ubo->SecondaryColor = Vec4f(0.1f, 0.1f, 0.1f, 1.0f);
        ubo->InnerColor = Vec4f(0.2f, 0.2f, 0.9f, 0.4f);

        // set viewport rect
        ubo->RectPosition = Vec2f(viewport.x, viewport.y);
        ubo->RectSize = Vec2f(viewport.width, viewport.height);

        ubo->TargetView = firstView != lastView ? viewIndex : -1;

        if(m_pDriver->GetExtensions(GetRecord(m_Device)).ext_AMD_negative_viewport_height ||
           m_pDriver->GetExtensions(GetRecord(m_Device)).ext_KHR_maintenance1)
        {
          ubo->RectSize.y = fabsf(viewport.height);

          // VK_KHR_maintenance1 requires the position to be adjusted as well
          if(m_pDriver->GetExtensions(GetRecord(m_Device)).ext_KHR_maintenance1 &&
             viewport.height < 0.0f)
            ubo->RectPosition.y += viewport.height;
        }

        m_Overlay.m_CheckerUBO.Unmap();

        vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS, Unwrap(checkerPipe));
        vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                  Unwrap(m_Overlay.m_CheckerPipeLayout), 0, 1,
                                  UnwrapPtr(m_Overlay.m_CheckerDescSet), 1, &uboOffs);

        vt->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);

        if(viewIndex < state.scissors.size())
        {
          Vec4f scissor((float)state.scissors[viewIndex].offset.x,
                        (float)state.scissors[viewIndex].offset.y,
                        (float)state.scissors[viewIndex].extent.width,
                        (float)state.scissors[viewIndex].extent.height);

          ubo = (CheckerboardUBOData *)m_Overlay.m_CheckerUBO.Map(&uboOffs);
          if(!ubo)
            return ResourceId();

          ubo->BorderWidth = 3;
          ubo->CheckerSquareDimension = 16.0f;

          // black/white checkered border
          ubo->PrimaryColor = Vec4f(1.0f, 1.0f, 1.0f, 1.0f);
          ubo->SecondaryColor = Vec4f(0.0f, 0.0f, 0.0f, 1.0f);

          // nothing at all inside
          ubo->InnerColor = Vec4f(0.0f, 0.0f, 0.0f, 0.0f);

          ubo->RectPosition = Vec2f(scissor.x, scissor.y);
          ubo->RectSize = Vec2f(scissor.z, scissor.w);

          ubo->TargetView = firstView != lastView ? viewIndex : -1;

          m_Overlay.m_CheckerUBO.Unmap();

          viewport.x = scissor.x;
          viewport.y = scissor.y;
          viewport.width = scissor.z;
          viewport.height = scissor.w;

          allviews.fill(state.views.count(), viewport);
          vt->CmdSetViewport(Unwrap(cmd), 0, allviews.count(), allviews.data());
          vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    Unwrap(m_Overlay.m_CheckerPipeLayout), 0, 1,
                                    UnwrapPtr(m_Overlay.m_CheckerDescSet), 1, &uboOffs);

          vt->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
        }
      }

      vt->CmdEndRenderPass(Unwrap(cmd));

      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      // submit & flush so that we don't have to keep pipeline around for a while
      m_pDriver->SubmitCmds();
      m_pDriver->FlushQ();

      for(int i = 0; i < 2; i++)
      {
        if(shad[i] != VK_NULL_HANDLE)
          m_pDriver->vkDestroyShaderEXT(m_Device, shad[i], NULL);
        m_pDriver->vkDestroyPipeline(m_Device, pipe[i], NULL);
        m_pDriver->vkDestroyShaderModule(m_Device, mod[i], NULL);
      }
      m_pDriver->vkDestroyPipeline(m_Device, checkerPipe, NULL);

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

      vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
      CHECK_VKR(m_pDriver, vkr);
    }
  }
  else if(overlay == DebugOverlay::BackfaceCull)
  {
    float highlightCol[] = {0.0f, 0.0f, 0.0f, 0.0f};

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)highlightCol, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);

    highlightCol[0] = 1.0f;
    highlightCol[1] = 0.0f;
    highlightCol[3] = 1.0f;

    if(!state.rastDiscardEnable)
    {
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      // backup state
      VulkanRenderState prevstate = state;

      // make patched shader
      VkShaderModule mod[2] = {0};
      VkPipeline pipe[2] = {0};
      VkShaderEXT shad[2] = {0};

      // first shader, no culling, writes red
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(mod[0], highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(shad[0], highlightCol);

      highlightCol[0] = 0.0f;
      highlightCol[1] = 1.0f;

      // second shader, normal culling, writes green
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(mod[1], highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(shad[1], highlightCol);

      // save original state
      VkCullModeFlags origCullMode = prevstate.cullMode;

      // make patched pipeline
      VkGraphicsPipelineCreateInfo pipeCreateInfo;

      if(!state.graphics.shaderObject)
      {
        m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                              prevstate.graphics.pipeline);

        // disable all tests possible
        VkPipelineDepthStencilStateCreateInfo *ds =
            (VkPipelineDepthStencilStateCreateInfo *)pipeCreateInfo.pDepthStencilState;
        ds->depthTestEnable = false;
        ds->depthWriteEnable = false;
        ds->stencilTestEnable = false;
        ds->depthBoundsTestEnable = false;

        VkPipelineRasterizationStateCreateInfo *rs =
            (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState;
        rs->cullMode = VK_CULL_MODE_NONE;    // first render without any culling
        rs->rasterizerDiscardEnable = false;

        VkPipelineMultisampleStateCreateInfo *msaa =
            (VkPipelineMultisampleStateCreateInfo *)pipeCreateInfo.pMultisampleState;
        msaa->pSampleMask = NULL;

        if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
          rs->depthClampEnable = true;

        VkPipelineColorBlendStateCreateInfo *cb =
            (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState;
        cb->logicOpEnable = false;
        cb->attachmentCount = 1;    // only one colour attachment
        for(uint32_t i = 0; i < cb->attachmentCount; i++)
        {
          VkPipelineColorBlendAttachmentState *att =
              (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
          att->blendEnable = false;
          att->colorWriteMask = 0xf;
        }

        // set our renderpass and shader
        pipeCreateInfo.renderPass = m_Overlay.NoDepthRP;
        pipeCreateInfo.subpass = 0;

        // don't use dynamic rendering
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO);
        // don't use custom resolve
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_CUSTOM_RESOLVE_CREATE_INFO_EXT);

        VkPipelineShaderStageCreateInfo *fragShader = NULL;

        for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++)
        {
          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i];
          if(sh.stage == VK_SHADER_STAGE_FRAGMENT_BIT)
          {
            sh.module = mod[0];
            sh.pName = "main";
            fragShader = &sh;
            break;
          }
        }

        if(fragShader == NULL)
        {
          // we know this is safe because it's pointing to a static array that's
          // big enough for all shaders

          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[pipeCreateInfo.stageCount++];
          sh.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
          sh.pNext = NULL;
          sh.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          sh.module = mod[0];
          sh.pName = "main";
          sh.pSpecializationInfo = NULL;

          fragShader = &sh;
        }

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &pipe[0]);
        CHECK_VKR(m_pDriver, vkr);

        fragShader->module = mod[1];
        rs->cullMode = origCullMode;

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &pipe[1]);
        CHECK_VKR(m_pDriver, vkr);
      }

      // disable tests in dynamic state too
      state.depthTestEnable = VK_FALSE;
      state.depthWriteEnable = VK_FALSE;
      state.depthCompareOp = VK_COMPARE_OP_ALWAYS;
      state.stencilTestEnable = VK_FALSE;
      state.depthBoundsTestEnable = VK_FALSE;
      state.cullMode = VK_CULL_MODE_NONE;

      state.sampleMask = {~0U};

      // enable dynamic depth clamp
      if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
        state.depthClampEnable = true;

      state.logicOpEnable = false;
      for(uint32_t i = 0; i < state.colorBlendEnable.size(); i++)
        state.colorBlendEnable[i] = false;
      for(uint32_t i = 0; i < state.colorWriteMask.size(); i++)
        state.colorWriteMask[i] = 0xf;

      // modify state
      state.SetRenderPass(GetResID(m_Overlay.NoDepthRP));
      state.subpass = 0;
      state.SetFramebuffer(m_pDriver, GetResID(m_Overlay.NoDepthFB));
      state.dynamicRendering.beginCustomResolve = false;

      state.graphics.pipeline = GetResID(pipe[0]);

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(shad[0]);
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      state.graphics.pipeline = GetResID(pipe[1]);
      state.cullMode = origCullMode;

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(shad[1]);
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      // submit & flush so that we don't have to keep pipeline around for a while
      m_pDriver->SubmitCmds();
      m_pDriver->FlushQ();

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

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

      // restore state
      state = prevstate;

      for(int i = 0; i < 2; i++)
      {
        if(shad[i] != VK_NULL_HANDLE)
          m_pDriver->vkDestroyShaderEXT(m_Device, shad[i], NULL);
        m_pDriver->vkDestroyPipeline(m_Device, pipe[i], NULL);
        m_pDriver->vkDestroyShaderModule(m_Device, mod[i], NULL);
      }
    }
  }
  else if(overlay == DebugOverlay::Depth || overlay == DebugOverlay::Stencil)
  {
    VkImage dsTempImage = VK_NULL_HANDLE;
    VkDeviceMemory dsTempImageMem = VK_NULL_HANDLE;
    VkImage dsDepthImage = VK_NULL_HANDLE;

    float highlightCol[] = {0.0f, 0.0f, 0.0f, 0.0f};

    {
      VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                      NULL,
                                      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                      VK_ACCESS_TRANSFER_WRITE_BIT,
                                      VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                      VK_QUEUE_FAMILY_IGNORED,
                                      VK_QUEUE_FAMILY_IGNORED,
                                      Unwrap(m_Overlay.Image),
                                      subRange};

      DoPipelineBarrier(cmd, 1, &barrier);

      vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image),
                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                             (VkClearColorValue *)highlightCol, 1, &subRange);

      std::swap(barrier.oldLayout, barrier.newLayout);
      std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
      barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

      DoPipelineBarrier(cmd, 1, &barrier);
    }

    if(!state.rastDiscardEnable)
    {
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);
      cmd = VK_NULL_HANDLE;

      VkFramebuffer depthFB = VK_NULL_HANDLE;
      VkRenderPass depthRP = VK_NULL_HANDLE;

      VulkanCreationInfo &createinfo = m_pDriver->m_CreationInfo;

      ResourceId depthStencilView;
      VkFormat dsFmt = VK_FORMAT_UNDEFINED;

      if(state.dynamicRendering.active)
      {
        depthStencilView = GetResID(state.dynamicRendering.depth.imageView);
        if(depthStencilView == ResourceId())
          depthStencilView = GetResID(state.dynamicRendering.stencil.imageView);
      }
      else
      {
        RDCASSERT(state.subpass < createinfo.m_RenderPass[state.GetRenderPass()].subpasses.size());
        int32_t dsIdx =
            createinfo.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].depthstencilAttachment;

        // make a renderpass and framebuffer for rendering to overlay color and using
        // depth buffer from the orignial render
        if(dsIdx >= 0 &&
           dsIdx < (int32_t)createinfo.m_Framebuffer[state.GetFramebuffer()].attachments.size())
        {
          depthStencilView = state.GetFramebufferAttachments()[dsIdx];
        }
      }

      bool useDepthWriteStencilPass = false;
      bool needDepthCopyToDepthStencil = false;

      size_t fmtIndex = ARRAY_COUNT(m_Overlay.m_DepthCopyPipeline);
      size_t sampleIndex = SampleIndex(iminfo.samples);

      if(depthStencilView != ResourceId())
      {
        if(overlay == DebugOverlay::Depth)
          useDepthWriteStencilPass = true;

        if(useDepthWriteStencilPass)
        {
          useDepthWriteStencilPass = false;
          const VulkanCreationInfo::ShaderEntry &ps =
              state.graphics.shaderObject ? createinfo.m_ShaderObject[state.shaderObjects[4]].shad
                                          : pipeInfo.shaders[4];
          if(ps.module != ResourceId())
          {
            const ShaderReflection *reflection = ps.refl;
            if(reflection)
            {
              for(const SigParameter &output : reflection->outputSignature)
              {
                if(output.systemValue == ShaderBuiltin::DepthOutput)
                  useDepthWriteStencilPass = true;
              }
            }
          }
        }

        VkAttachmentDescription attDescs[] = {
            {0, overlayFormat, VK_SAMPLE_COUNT_1_BIT, VK_ATTACHMENT_LOAD_OP_LOAD,
             VK_ATTACHMENT_STORE_OP_STORE, VK_ATTACHMENT_LOAD_OP_DONT_CARE,
             VK_ATTACHMENT_STORE_OP_DONT_CARE, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
             VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL},
            {0, VK_FORMAT_UNDEFINED, VK_SAMPLE_COUNT_1_BIT,    // will patch this just below
             VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_STORE, VK_ATTACHMENT_LOAD_OP_LOAD,
             VK_ATTACHMENT_STORE_OP_STORE, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
             VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL},
        };

        VulkanCreationInfo::ImageView &depthViewInfo = createinfo.m_ImageView[depthStencilView];

        ResourceId depthIm = depthViewInfo.image;
        VulkanCreationInfo::Image &depthImageInfo = createinfo.m_Image[depthIm];
        dsDepthImage = m_pDriver->GetResourceManager()->GetHandle<VkImage>(depthIm);

        dsFmt = depthImageInfo.format;
        VkFormat dsNewFmt = dsFmt;
        if(useDepthWriteStencilPass)
        {
          if(dsFmt == VK_FORMAT_D32_SFLOAT_S8_UINT)
            dsNewFmt = VK_FORMAT_D32_SFLOAT_S8_UINT;
          else if(dsFmt == VK_FORMAT_D24_UNORM_S8_UINT)
            dsNewFmt = VK_FORMAT_D24_UNORM_S8_UINT;
          else if(dsFmt == VK_FORMAT_D32_SFLOAT)
            dsNewFmt = VK_FORMAT_D32_SFLOAT_S8_UINT;
          else if(dsFmt == VK_FORMAT_D16_UNORM)
            dsNewFmt = m_Overlay.m_DefaultDepthStencilFormat;
          else
            dsNewFmt = m_Overlay.m_DefaultDepthStencilFormat;

          RDCASSERT((dsNewFmt == VK_FORMAT_D24_UNORM_S8_UINT) ||
                    (dsNewFmt == VK_FORMAT_D32_SFLOAT_S8_UINT));
          fmtIndex = (dsNewFmt == VK_FORMAT_D24_UNORM_S8_UINT) ? 0 : 1;
          if(m_Overlay.m_DepthResolvePipeline[fmtIndex][sampleIndex] == 0)
          {
            RDCERR("Unhandled depth resolve format : %s", ToStr(dsNewFmt).c_str());
            useDepthWriteStencilPass = false;
          }
          if(dsNewFmt != dsFmt)
          {
            needDepthCopyToDepthStencil = true;
            if(m_Overlay.m_DepthCopyPipeline[fmtIndex][sampleIndex] == 0)
            {
              RDCERR("Unhandled depth copy format : %s", ToStr(dsNewFmt).c_str());
              useDepthWriteStencilPass = false;
              needDepthCopyToDepthStencil = false;
            }
          }
          // Currently depth-copy is only supported for Texture2D and Texture2DMS
          if(dsFmt != dsNewFmt)
          {
            if(depthImageInfo.type != VK_IMAGE_TYPE_2D)
              useDepthWriteStencilPass = false;
          }
          if(!useDepthWriteStencilPass)
          {
            RDCWARN("Depth overlay using fallback method instead of stencil mask");
            dsNewFmt = dsFmt;
          }
        }

        attDescs[1].format = dsNewFmt;
        attDescs[0].samples = attDescs[1].samples = iminfo.samples;

        {
          LockedConstImageStateRef imState = m_pDriver->FindConstImageState(depthIm);
          if(imState)
          {
            // find the state that overlaps the view's subresource range start. We assume all
            // subresources are correctly in the same state (as they should be) so we just need to
            // find the first match.
            auto it = imState->subresourceStates.RangeBegin(depthViewInfo.range);
            if(it != imState->subresourceStates.end())
              attDescs[1].initialLayout = attDescs[1].finalLayout = it->state().newLayout;
          }
        }

        VkAttachmentReference colRef = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
        VkAttachmentReference dsRef = {1, attDescs[1].initialLayout};

        VkSubpassDescription subp = {
            0,      VK_PIPELINE_BIND_POINT_GRAPHICS,
            0,      NULL,       // inputs
            1,      &colRef,    // color
            NULL,               // resolve
            &dsRef,             // depth-stencil
            0,      NULL,       // preserve
        };

        VkRenderPassCreateInfo rpinfo = {
            VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
            NULL,
            0,
            2,
            attDescs,
            1,
            &subp,
            0,
            NULL,    // dependencies
        };

        if(multiviewMask > 0)
          rpinfo.pNext = &multiviewRP;

        vkr = m_pDriver->vkCreateRenderPass(m_Device, &rpinfo, NULL, &depthRP);
        CHECK_VKR(m_pDriver, vkr);

        VkImageView dsView =
            m_pDriver->GetResourceManager()->GetHandle<VkImageView>(depthStencilView);

        if(needDepthCopyToDepthStencil)
        {
          VkImageSubresourceRange dsSubRange = {
              VK_IMAGE_ASPECT_DEPTH_BIT, sub.mip, 1, sub.slice, sub.numSlices,
          };

          VkImageViewCreateInfo dsViewInfo = {
              VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
              NULL,
              0,
              dsDepthImage,
              VK_IMAGE_VIEW_TYPE_2D,
              dsFmt,
              {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
               VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
              dsSubRange,
          };

          vkr = m_pDriver->vkCreateImageView(m_Device, &dsViewInfo, NULL, &dsView);

          // update descriptor to point to copy of original depth buffer
          VkDescriptorImageInfo imdesc = {0};
          imdesc.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
          imdesc.sampler = VK_NULL_HANDLE;
          imdesc.imageView = Unwrap(dsView);

          VkWriteDescriptorSet write = {
              VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
              NULL,
              Unwrap(m_Overlay.m_DepthCopyDescSet),
              0,
              0,
              1,
              VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
              &imdesc,
              NULL,
              NULL,
          };
          vt->UpdateDescriptorSets(Unwrap(m_Device), 1, &write, 0, NULL);

          // Create texture for new depth buffer
          VkImageCreateInfo dsNewImInfo = {
              VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
              NULL,
              0,
              VK_IMAGE_TYPE_2D,
              dsNewFmt,
              depthImageInfo.extent,
              (uint32_t)depthImageInfo.mipLevels,
              (uint32_t)depthImageInfo.arrayLayers,
              depthImageInfo.samples,
              VK_IMAGE_TILING_OPTIMAL,
              VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
                  VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
              VK_SHARING_MODE_EXCLUSIVE,
              0,
              NULL,
              VK_IMAGE_LAYOUT_UNDEFINED,
          };

          vkr = m_pDriver->vkCreateImage(m_Device, &dsNewImInfo, NULL, &dsTempImage);
          CHECK_VKR(m_pDriver, vkr);

          NameVulkanObject(dsTempImage, "Overlay Depth+Stencil Image");

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

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

          vkr = m_pDriver->vkAllocateMemory(m_Device, &allocInfo, NULL, &dsTempImageMem);
          CHECK_VKR(m_pDriver, vkr);

          if(vkr != VK_SUCCESS)
            return ResourceId();

          vkr = m_pDriver->vkBindImageMemory(m_Device, dsTempImage, dsTempImageMem, 0);
          CHECK_VKR(m_pDriver, vkr);

          cmd = m_pDriver->GetNextCmd();
          if(cmd == VK_NULL_HANDLE)
            return ResourceId();

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

          // move original depth buffer to shader read state
          m_pDriver->FindImageState(depthIm)->InlineTransition(
              cmd, m_pDriver->m_QueueFamilyIdx, VK_IMAGE_LAYOUT_GENERAL,
              VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
              m_pDriver->GetImageTransitionInfo());

          // transition new depth buffer to depth write state
          m_pDriver->FindImageState(GetResID(dsTempImage))
              ->InlineTransition(cmd, m_pDriver->m_QueueFamilyIdx, VK_IMAGE_LAYOUT_GENERAL, 0,
                                 VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
                                 m_pDriver->GetImageTransitionInfo());

          vkr = vt->EndCommandBuffer(Unwrap(cmd));
          CHECK_VKR(m_pDriver, vkr);
          cmd = VK_NULL_HANDLE;

          dsSubRange = {
              VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT,
              sub.mip,
              1,
              sub.slice,
              sub.numSlices,
          };

          dsViewInfo = {
              VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
              NULL,
              0,
              dsTempImage,
              VK_IMAGE_VIEW_TYPE_2D,
              dsNewFmt,
              {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
               VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
              dsSubRange,
          };

          vkr = m_pDriver->vkCreateImageView(m_Device, &dsViewInfo, NULL, &dsView);
          CHECK_VKR(m_pDriver, vkr);
          dsDepthImage = dsTempImage;
        }

        VkImageView views[] = {
            m_Overlay.ImageView,
            dsView,
        };

        // Create framebuffer rendering just to overlay image, no depth
        VkFramebufferCreateInfo fbinfo = {
            VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
            NULL,
            0,
            depthRP,
            2,
            views,
            (uint32_t)m_Overlay.ImageDim.width,
            (uint32_t)m_Overlay.ImageDim.height,
            1,
        };

        vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &depthFB);
        CHECK_VKR(m_pDriver, vkr);

        // Fullscreen pass using shader to copy original depth buffer -> new depth buffer
        // Pipeline also writes 0 to the stencil during the pass
        if(needDepthCopyToDepthStencil)
        {
          cmd = m_pDriver->GetNextCmd();
          if(cmd == VK_NULL_HANDLE)
            return ResourceId();

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

          VkClearValue clearval = {};
          VkRenderPassBeginInfo rpbegin = {
              VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
              NULL,
              Unwrap(depthRP),
              Unwrap(depthFB),
              state.renderArea,
              1,
              &clearval,
          };
          vt->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

          vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                              Unwrap(m_Overlay.m_DepthCopyPipeline[fmtIndex][sampleIndex]));
          vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    Unwrap(m_Overlay.m_DepthCopyPipeLayout), 0, 1,
                                    UnwrapPtr(m_Overlay.m_DepthCopyDescSet), 0, NULL);

          VkViewport viewport = {
              0.0f, 0.0f, (float)m_Overlay.ImageDim.width, (float)m_Overlay.ImageDim.height,
              0.0f, 1.0f,
          };
          vt->CmdSetViewport(Unwrap(cmd), 0, 1, &viewport);

          vt->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
          vt->CmdEndRenderPass(Unwrap(cmd));

          vkr = vt->EndCommandBuffer(Unwrap(cmd));
          CHECK_VKR(m_pDriver, vkr);
          cmd = VK_NULL_HANDLE;
        }
        dsFmt = dsNewFmt;
      }

      // if depthRP is NULL, so is depthFB, and it means no depth buffer was
      // bound, so we just render green.

      highlightCol[0] = 1.0f;
      highlightCol[1] = 0.0f;
      highlightCol[3] = 1.0f;

      // backup state
      VulkanRenderState prevstate = state;

      // make patched shader
      VkShaderModule failmod = {}, passmod = {};
      VkShaderEXT failshad = {}, passshad = {};
      VkPipeline failpipe = {}, passpipe = {}, depthWriteStencilPipe = {};

      // first shader, no depth/stencil testing, writes red
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(failmod, highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(failshad, highlightCol);

      highlightCol[0] = 0.0f;
      highlightCol[1] = 1.0f;

      // second shader, enabled depth/stencil testing, writes green
      if(!state.graphics.shaderObject)
        GetDebugManager()->PatchFixedColShader(passmod, highlightCol);
      else
        GetDebugManager()->PatchFixedColShaderObject(passshad, highlightCol);

      // save original state
      VkBool32 origDepthTest = prevstate.depthTestEnable;
      VkBool32 origDepthBoundsTest = prevstate.depthBoundsTestEnable;
      VkBool32 origStencilTest = prevstate.stencilTestEnable;

      // make patched pipeline
      VkGraphicsPipelineCreateInfo pipeCreateInfo;

      m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                            prevstate.graphics.pipeline);

      if(!state.graphics.shaderObject)
      {
        // disable all tests possible
        VkPipelineDepthStencilStateCreateInfo *ds =
            (VkPipelineDepthStencilStateCreateInfo *)pipeCreateInfo.pDepthStencilState;
        ds->depthTestEnable = false;
        ds->depthWriteEnable = false;
        ds->stencilTestEnable = false;
        ds->depthBoundsTestEnable = false;

        VkPipelineMultisampleStateCreateInfo *msaa =
            (VkPipelineMultisampleStateCreateInfo *)pipeCreateInfo.pMultisampleState;
        msaa->pSampleMask = NULL;

        VkPipelineColorBlendStateCreateInfo *cb =
            (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState;
        cb->logicOpEnable = false;
        cb->attachmentCount = 1;    // only one colour attachment
        for(uint32_t i = 0; i < cb->attachmentCount; i++)
        {
          VkPipelineColorBlendAttachmentState *att =
              (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
          att->blendEnable = false;
          att->colorWriteMask = 0xf;
        }

        // subpass 0 in either render pass
        pipeCreateInfo.subpass = 0;

        VkPipelineShaderStageCreateInfo orgFragShader = {};
        VkPipelineShaderStageCreateInfo *fragShader = NULL;

        for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++)
        {
          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[i];
          if(sh.stage == VK_SHADER_STAGE_FRAGMENT_BIT)
          {
            orgFragShader = sh;
            sh.pName = "main";
            fragShader = &sh;
            break;
          }
        }

        if(fragShader == NULL)
        {
          useDepthWriteStencilPass = false;
          // we know this is safe because it's pointing to a static array that's
          // big enough for all shaders

          VkPipelineShaderStageCreateInfo &sh =
              (VkPipelineShaderStageCreateInfo &)pipeCreateInfo.pStages[pipeCreateInfo.stageCount++];
          sh.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
          sh.pNext = NULL;
          sh.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          sh.pName = "main";
          sh.pSpecializationInfo = NULL;

          fragShader = &sh;
        }

        fragShader->module = passmod;

        if(depthRP != VK_NULL_HANDLE)
        {
          if(overlay == DebugOverlay::Depth)
          {
            ds->depthTestEnable = origDepthTest;
            ds->depthBoundsTestEnable = origDepthBoundsTest;
          }
          else
          {
            ds->front.passOp = ds->front.failOp = ds->front.depthFailOp = VK_STENCIL_OP_KEEP;
            ds->back.passOp = ds->back.failOp = ds->back.depthFailOp = VK_STENCIL_OP_KEEP;
            ds->stencilTestEnable = origStencilTest;
          }
          pipeCreateInfo.renderPass = depthRP;
        }
        else
        {
          pipeCreateInfo.renderPass = m_Overlay.NoDepthRP;
        }

        // don't use dynamic rendering
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO);
        // don't use custom resolve
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_CUSTOM_RESOLVE_CREATE_INFO_EXT);

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &passpipe);
        CHECK_VKR(m_pDriver, vkr);

        fragShader->module = failmod;

        // set our renderpass and shader
        pipeCreateInfo.renderPass = m_Overlay.NoDepthRP;

        // disable culling/discard and enable depth clamp. That way we show any failures due to these
        VkPipelineRasterizationStateCreateInfo *rs =
            (VkPipelineRasterizationStateCreateInfo *)pipeCreateInfo.pRasterizationState;
        VkPipelineRasterizationStateCreateInfo orgRS = *rs;
        rs->cullMode = VK_CULL_MODE_NONE;
        rs->rasterizerDiscardEnable = false;

        if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
          rs->depthClampEnable = true;

        vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                   NULL, &failpipe);
        CHECK_VKR(m_pDriver, vkr);

        if(useDepthWriteStencilPass)
        {
          pipeCreateInfo.renderPass = depthRP;
          *rs = orgRS;

          // disable colour write
          for(uint32_t i = 0; i < cb->attachmentCount; i++)
          {
            VkPipelineColorBlendAttachmentState *att =
                (VkPipelineColorBlendAttachmentState *)&cb->pAttachments[i];
            att->blendEnable = false;
            att->colorWriteMask = 0x0;
          }

          // Write stencil 0x1 for depth passing pixels
          ds->stencilTestEnable = true;
          ds->front.compareOp = VK_COMPARE_OP_ALWAYS;
          ds->front.failOp = VK_STENCIL_OP_KEEP;
          ds->front.depthFailOp = VK_STENCIL_OP_KEEP;
          ds->front.passOp = VK_STENCIL_OP_REPLACE;
          ds->front.compareMask = 0xff;
          ds->front.reference = 0x1;
          ds->front.writeMask = 0xff;
          ds->back = ds->front;

          // Use original shader
          *fragShader = orgFragShader;

          vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &pipeCreateInfo,
                                                     NULL, &depthWriteStencilPipe);
          CHECK_VKR(m_pDriver, vkr);
        }
      }

      // modify state
      state.SetRenderPass(GetResID(m_Overlay.NoDepthRP));
      state.subpass = 0;
      state.SetFramebuffer(m_pDriver, GetResID(m_Overlay.NoDepthFB));
      state.dynamicRendering.beginCustomResolve = false;

      state.graphics.pipeline = GetResID(failpipe);

      // disable tests in dynamic state too
      state.depthTestEnable = VK_FALSE;
      state.depthWriteEnable = VK_FALSE;
      state.stencilTestEnable = VK_FALSE;
      state.depthBoundsTestEnable = VK_FALSE;
      state.cullMode = VK_CULL_MODE_NONE;

      state.sampleMask = {~0U};

      // enable dynamic depth clamp
      if(m_pDriver->GetDeviceEnabledFeatures().depthClamp)
        state.depthClampEnable = true;

      state.logicOpEnable = false;
      for(uint32_t i = 0; i < state.colorBlendEnable.size(); i++)
        state.colorBlendEnable[i] = false;
      for(uint32_t i = 0; i < state.colorWriteMask.size(); i++)
        state.colorWriteMask[i] = 0xf;

      if(depthRP != VK_NULL_HANDLE)
      {
        if(overlay == DebugOverlay::Depth)
        {
          state.depthTestEnable = origDepthTest;
          state.depthBoundsTestEnable = origDepthBoundsTest;
        }
        else
        {
          state.front.passOp = state.front.failOp = state.front.depthFailOp = VK_STENCIL_OP_KEEP;
          state.back.passOp = state.back.failOp = state.back.depthFailOp = VK_STENCIL_OP_KEEP;
          state.stencilTestEnable = origStencilTest;
        }
      }

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(failshad);
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      if(useDepthWriteStencilPass)
      {
        // disable colour write
        for(uint32_t i = 0; i < state.colorWriteMask.size(); i++)
          state.colorWriteMask[i] = 0x0;

        // override stencil dynamic state
        state.front.compare = 0xff;
        state.front.write = 0xff;
        state.front.ref = 0x1;
        state.front.failOp = VK_STENCIL_OP_KEEP;
        state.front.passOp = VK_STENCIL_OP_REPLACE;
        state.front.depthFailOp = VK_STENCIL_OP_KEEP;
        state.front.compare = VK_COMPARE_OP_ALWAYS;
        state.back = state.front;
        state.graphics.pipeline = GetResID(depthWriteStencilPipe);
      }
      else
      {
        state.graphics.pipeline = GetResID(passpipe);
      }

      if(depthRP != VK_NULL_HANDLE)
      {
        state.SetRenderPass(GetResID(depthRP));
        state.SetFramebuffer(m_pDriver, GetResID(depthFB));
      }

      if(overlay == DebugOverlay::Depth)
      {
        state.depthTestEnable = origDepthTest;
        state.depthBoundsTestEnable = origDepthBoundsTest;
      }
      else
      {
        state.stencilTestEnable = origStencilTest;
      }

      if(useDepthWriteStencilPass)
      {
        cmd = m_pDriver->GetNextCmd();
        if(cmd == VK_NULL_HANDLE)
          return ResourceId();

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

        VkImageSubresourceRange sSubRange = {
            VK_IMAGE_ASPECT_STENCIL_BIT, sub.mip, 1, sub.slice, sub.numSlices,
        };
        VkImageSubresourceRange dsSubRange = sSubRange;
        dsSubRange.aspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT;

        VkClearDepthStencilValue depthStencilClear = {};
        depthStencilClear.depth = 0.0f;
        depthStencilClear.stencil = 0;

        {
          VkImageLayout startLayout = m_pDriver->GetDebugManager()->GetImageLayout(
              GetResID(dsDepthImage), (VkImageAspectFlagBits)dsSubRange.aspectMask,
              dsSubRange.baseMipLevel, dsSubRange.baseArrayLayer);

          VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                          NULL,
                                          VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
                                          VK_ACCESS_TRANSFER_WRITE_BIT,
                                          startLayout,
                                          VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                          VK_QUEUE_FAMILY_IGNORED,
                                          VK_QUEUE_FAMILY_IGNORED,
                                          Unwrap(dsDepthImage),
                                          dsSubRange};
          DoPipelineBarrier(cmd, 1, &barrier);

          vt->CmdClearDepthStencilImage(Unwrap(cmd), Unwrap(dsDepthImage),
                                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &depthStencilClear, 1,
                                        &sSubRange);
          std::swap(barrier.oldLayout, barrier.newLayout);
          std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
          DoPipelineBarrier(cmd, 1, &barrier);
        }

        vkr = vt->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);
        cmd = VK_NULL_HANDLE;
      }

      if(state.graphics.shaderObject)
      {
        state.graphics.pipeline = ResourceId();
        state.shaderObjects[4] = GetResID(passshad);
      }

      m_pDriver->ReplayLog(0, eventId, eReplay_OnlyDraw);

      if(useDepthWriteStencilPass)
      {
        // Resolve stencil = 0x1 pixels to green
        cmd = m_pDriver->GetNextCmd();

        if(cmd == VK_NULL_HANDLE)
          return ResourceId();

        RDCASSERT((dsFmt == VK_FORMAT_D24_UNORM_S8_UINT) || (dsFmt == VK_FORMAT_D32_SFLOAT_S8_UINT));
        vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
        CHECK_VKR(m_pDriver, vkr);

        VkClearValue clearval = {};
        VkRenderPassBeginInfo rpbegin = {
            VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
            NULL,
            Unwrap(depthRP),
            Unwrap(depthFB),
            state.renderArea,
            1,
            &clearval,
        };
        vt->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

        vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                            Unwrap(m_Overlay.m_DepthResolvePipeline[fmtIndex][sampleIndex]));

        VkViewport viewport = {
            0.0f, 0.0f, (float)m_Overlay.ImageDim.width, (float)m_Overlay.ImageDim.height,
            0.0f, 1.0f,
        };
        vt->CmdSetViewport(Unwrap(cmd), 0, 1, &viewport);

        vt->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
        vt->CmdEndRenderPass(Unwrap(cmd));

        vkr = vt->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);
        cmd = VK_NULL_HANDLE;
      }

      // submit & flush so that we don't have to keep pipeline around for a while
      m_pDriver->SubmitCmds();
      m_pDriver->FlushQ();

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

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

      // restore state
      state = prevstate;

      m_pDriver->vkDestroyPipeline(m_Device, failpipe, NULL);
      m_pDriver->vkDestroyShaderModule(m_Device, failmod, NULL);
      m_pDriver->vkDestroyPipeline(m_Device, passpipe, NULL);
      m_pDriver->vkDestroyShaderModule(m_Device, passmod, NULL);
      m_pDriver->vkDestroyPipeline(m_Device, depthWriteStencilPipe, NULL);
      m_pDriver->vkDestroyImage(m_Device, dsTempImage, NULL);
      m_pDriver->vkFreeMemory(m_Device, dsTempImageMem, NULL);

      if(failshad != VK_NULL_HANDLE)
        m_pDriver->vkDestroyShaderEXT(m_Device, failshad, NULL);
      if(passshad != VK_NULL_HANDLE)
        m_pDriver->vkDestroyShaderEXT(m_Device, passshad, NULL);

      if(depthRP != VK_NULL_HANDLE)
      {
        m_pDriver->vkDestroyRenderPass(m_Device, depthRP, NULL);
        m_pDriver->vkDestroyFramebuffer(m_Device, depthFB, NULL);
      }
    }
  }
  else if(overlay == DebugOverlay::ClearBeforeDraw || overlay == DebugOverlay::ClearBeforePass)
  {
    // clear the overlay image itself
    float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

    VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                    NULL,
                                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                    VK_ACCESS_TRANSFER_WRITE_BIT,
                                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    VK_QUEUE_FAMILY_IGNORED,
                                    Unwrap(m_Overlay.Image),
                                    subRange};

    DoPipelineBarrier(cmd, 1, &barrier);

    vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                           (VkClearColorValue *)black, 1, &subRange);

    std::swap(barrier.oldLayout, barrier.newLayout);
    std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
    barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

    DoPipelineBarrier(cmd, 1, &barrier);

    rdcarray<uint32_t> events = passEvents;

    if(overlay == DebugOverlay::ClearBeforeDraw)
      events.clear();

    events.push_back(eventId);

    {
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      if(Vulkan_Debug_SingleSubmitFlushing())
        m_pDriver->SubmitCmds();

      size_t startEvent = 0;

      // if we're ClearBeforePass the first event will be a vkBeginRenderPass.
      // if there are any other events, we need to play up to right before them
      // so that we have all the render state set up to do
      // BeginRenderPassAndApplyState and a clear. If it's just the begin, we
      // just play including it, do the clear, then we won't replay anything
      // in the loop below
      if(overlay == DebugOverlay::ClearBeforePass)
      {
        const ActionDescription *action = m_pDriver->GetAction(events[0]);
        if(action && action->flags & ActionFlags::BeginPass)
        {
          if(events.size() == 1)
          {
            m_pDriver->ReplayLog(0, events[0], eReplay_Full);
          }
          else
          {
            startEvent = 1;
            m_pDriver->ReplayLog(0, events[1], eReplay_WithoutDraw);
          }
        }
      }
      else
      {
        m_pDriver->ReplayLog(0, events[0], eReplay_WithoutDraw);
      }

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

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

      state.BeginRenderPassAndApplyState(m_pDriver, cmd, VulkanRenderState::BindGraphics, false);

      VkClearAttachment clearatt = {VK_IMAGE_ASPECT_COLOR_BIT, 0, {}};
      memcpy(clearatt.clearValue.color.float32, &clearCol, sizeof(clearatt.clearValue.color.float32));
      rdcarray<VkClearAttachment> atts;

      if(state.dynamicRendering.active)
      {
        for(size_t i = 0; i < state.dynamicRendering.color.size(); i++)
        {
          if(state.dynamicRendering.color[i].imageView == VK_NULL_HANDLE)
            continue;

          clearatt.colorAttachment = (uint32_t)i;
          atts.push_back(clearatt);
        }
      }
      else
      {
        VulkanCreationInfo::RenderPass &rp =
            m_pDriver->m_CreationInfo.m_RenderPass[state.GetRenderPass()];

        for(size_t i = 0; i < rp.subpasses[state.subpass].colorAttachments.size(); i++)
        {
          clearatt.colorAttachment = (uint32_t)i;
          atts.push_back(clearatt);
        }
      }

      // Try to clear depth as well, to help debug shadow rendering
      if((state.graphics.pipeline != ResourceId() || state.graphics.shaderObject) &&
         IsDepthOrStencilFormat(iminfo.format))
      {
        VkCompareOp depthCompareOp = state.depthCompareOp;

        // If the depth func is equal or not equal, don't clear at all since the output would be
        // altered in an way that would cause replay to produce mostly incorrect results.
        // Similarly, skip if the depth func is always, as we'd have a 50% chance of guessing the
        // wrong clear value.
        if(depthCompareOp != VK_COMPARE_OP_EQUAL && depthCompareOp != VK_COMPARE_OP_NOT_EQUAL &&
           depthCompareOp != VK_COMPARE_OP_ALWAYS)
        {
          // If the depth func is less or less equal, clear to 1 instead of 0
          bool depthFuncLess =
              depthCompareOp == VK_COMPARE_OP_LESS || depthCompareOp == VK_COMPARE_OP_LESS_OR_EQUAL;
          float depthClear = depthFuncLess ? 1.0f : 0.0f;

          VkClearAttachment clearDepthAtt = {
              VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, {}};
          clearDepthAtt.clearValue.depthStencil.depth = depthClear;
          clearDepthAtt.clearValue.depthStencil.stencil = 0;

          atts.push_back(clearDepthAtt);
        }
      }

      VkClearRect rect = {
          state.renderArea,
          0,
          1,
      };

      vt->CmdClearAttachments(Unwrap(cmd), (uint32_t)atts.size(), &atts[0], 1, &rect);

      state.EndRenderPass(cmd);

      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      for(size_t i = startEvent; i < events.size(); i++)
      {
        m_pDriver->ReplayLog(events[i], events[i], eReplay_OnlyDraw);

        if(overlay == DebugOverlay::ClearBeforePass && i + 1 < events.size())
          m_pDriver->ReplayLog(events[i] + 1, events[i + 1], eReplay_WithoutDraw);
      }

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

      vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
      CHECK_VKR(m_pDriver, vkr);
    }
  }
  else if(overlay == DebugOverlay::QuadOverdrawPass || overlay == DebugOverlay::QuadOverdrawDraw)
  {
    if(m_Overlay.m_QuadResolvePipeline[0] != VK_NULL_HANDLE && !state.rastDiscardEnable &&
       m_Overlay.Samples == VK_SAMPLE_COUNT_1_BIT)
    {
      VulkanRenderState prevstate = state;

      SCOPED_TIMER("Quad Overdraw");

      float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

      VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                      NULL,
                                      VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                      VK_ACCESS_TRANSFER_WRITE_BIT,
                                      VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                      VK_QUEUE_FAMILY_IGNORED,
                                      VK_QUEUE_FAMILY_IGNORED,
                                      Unwrap(m_Overlay.Image),
                                      subRange};

      DoPipelineBarrier(cmd, 1, &barrier);

      vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image),
                             VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, (VkClearColorValue *)black, 1,
                             &subRange);

      std::swap(barrier.oldLayout, barrier.newLayout);
      std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
      barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

      DoPipelineBarrier(cmd, 1, &barrier);

      rdcarray<uint32_t> events = passEvents;

      if(overlay == DebugOverlay::QuadOverdrawDraw)
        events.clear();

      events.push_back(eventId);

      // if we're rendering the whole pass, and the first action is a BeginRenderPass, don't include
      // it in the list. We want to start by replaying into the renderpass so that we have the
      // correct state being applied.
      if(overlay == DebugOverlay::QuadOverdrawPass)
      {
        const ActionDescription *action = m_pDriver->GetAction(events[0]);
        if(action->flags & ActionFlags::BeginPass)
          events.erase(0);
      }

      VkPipeline quadResolveMultiviewPipe = VK_NULL_HANDLE;
      VkImage quadImg;
      VkDeviceMemory quadImgMem;
      VkImageView quadImgView;

      uint32_t quadImgArraySlices = 4;
      if(multiviewMask > 0)
      {
        quadImgArraySlices *= Bits::CountOnes(multiviewMask);

        quadResolveMultiviewPipe = m_Overlay.CreateTempMultiviewQuadResolvePipe(m_pDriver);
      }

      VkImageCreateInfo imInfo = {
          VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
          NULL,
          0,
          VK_IMAGE_TYPE_2D,
          VK_FORMAT_R32_UINT,
          {RDCMAX(1U, m_Overlay.ImageDim.width >> 1), RDCMAX(1U, m_Overlay.ImageDim.height >> 1), 1},
          1,
          quadImgArraySlices,
          VK_SAMPLE_COUNT_1_BIT,
          VK_IMAGE_TILING_OPTIMAL,
          VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
          VK_SHARING_MODE_EXCLUSIVE,
          0,
          NULL,
          VK_IMAGE_LAYOUT_UNDEFINED,
      };

      vkr = m_pDriver->vkCreateImage(m_Device, &imInfo, NULL, &quadImg);
      CHECK_VKR(m_pDriver, vkr);

      NameVulkanObject(quadImg, "m_Overlay.quadImg");

      VkMemoryRequirements mrq = {0};

      m_pDriver->vkGetImageMemoryRequirements(m_Device, quadImg, &mrq);

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

      vkr = m_pDriver->vkAllocateMemory(m_Device, &allocInfo, NULL, &quadImgMem);
      CHECK_VKR(m_pDriver, vkr);

      if(vkr != VK_SUCCESS)
        return ResourceId();

      vkr = m_pDriver->vkBindImageMemory(m_Device, quadImg, quadImgMem, 0);
      CHECK_VKR(m_pDriver, vkr);

      VkImageViewCreateInfo viewinfo = {
          VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
          NULL,
          0,
          quadImg,
          VK_IMAGE_VIEW_TYPE_2D_ARRAY,
          VK_FORMAT_R32_UINT,
          {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
           VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY},
          {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, quadImgArraySlices},
      };

      vkr = m_pDriver->vkCreateImageView(m_Device, &viewinfo, NULL, &quadImgView);
      CHECK_VKR(m_pDriver, vkr);

      // update descriptor to point to our R32 result image
      VkDescriptorImageInfo imdesc = {0};
      imdesc.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
      imdesc.sampler = VK_NULL_HANDLE;
      imdesc.imageView = Unwrap(quadImgView);

      VkWriteDescriptorSet write = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
                                    NULL,
                                    Unwrap(m_Overlay.m_QuadDescSet),
                                    0,
                                    0,
                                    1,
                                    VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
                                    &imdesc,
                                    NULL,
                                    NULL};
      vt->UpdateDescriptorSets(Unwrap(m_Device), 1, &write, 0, NULL);

      VkImageMemoryBarrier quadImBarrier = {
          VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
          NULL,
          0,
          VK_ACCESS_TRANSFER_WRITE_BIT,
          VK_IMAGE_LAYOUT_UNDEFINED,
          VK_IMAGE_LAYOUT_GENERAL,
          VK_QUEUE_FAMILY_IGNORED,
          VK_QUEUE_FAMILY_IGNORED,
          Unwrap(quadImg),
          {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, quadImgArraySlices},
      };

      // clear all to black
      DoPipelineBarrier(cmd, 1, &quadImBarrier);
      vt->CmdClearColorImage(Unwrap(cmd), Unwrap(quadImg), VK_IMAGE_LAYOUT_GENERAL,
                             (VkClearColorValue *)&black, 1, &quadImBarrier.subresourceRange);

      quadImBarrier.srcAccessMask = quadImBarrier.dstAccessMask;
      quadImBarrier.oldLayout = quadImBarrier.newLayout;

      quadImBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;

      // set to general layout, for load/store operations
      DoPipelineBarrier(cmd, 1, &quadImBarrier);

      VkMemoryBarrier memBarrier = {
          VK_STRUCTURE_TYPE_MEMORY_BARRIER,
          NULL,
          VK_ACCESS_ALL_WRITE_BITS,
          VK_ACCESS_ALL_READ_BITS,
      };

      DoPipelineBarrier(cmd, 1, &memBarrier);

      // end this cmd buffer so the image is in the right state for the next part
      vkr = vt->EndCommandBuffer(Unwrap(cmd));
      CHECK_VKR(m_pDriver, vkr);

      if(Vulkan_Debug_SingleSubmitFlushing())
        m_pDriver->SubmitCmds();

      m_pDriver->ReplayLog(0, events[0], eReplay_WithoutDraw);

      // fill descriptor here so that initial contents doesn't overwrite it
      if(m_pDriver->DescriptorBuffers())
      {
        VkDescriptorGetInfoEXT info = {
            VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT,
            NULL,
        };

        VkDescriptorImageInfo imginfo = {};
        info.type = write.descriptorType;
        info.data.pStorageImage = &imdesc;

        VkDeviceSize offs = 0;
        vt->GetDescriptorSetLayoutBindingOffsetEXT(Unwrap(m_Device),
                                                   Unwrap(m_Overlay.m_QuadDescBufLayout), 0, &offs);
        uint32_t size = m_pDriver->DescriptorDataSize(info.type);
        vt->GetDescriptorEXT(Unwrap(m_Device), &info, size,
                             ((byte *)m_Overlay.m_QuadDescriptor.Map()) + offs);
        m_Overlay.m_QuadDescriptor.Unmap();

        cmd = m_pDriver->GetNextCmd();

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

        // since we don't know which resource descriptor buffers the application is going to use when,
        // we copy our descriptor into the end of every single one so it will be available no matter what
        m_pDriver->CopyInternalDescriptor(Unwrap(cmd), m_Overlay.m_QuadDescriptor.UnwrappedBuffer(),
                                          size);

        vkr = vt->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);

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

      {
        // declare callback struct here
        VulkanQuadOverdrawCallback cb(m_pDriver, m_Overlay.m_QuadDescSetLayout,
                                      m_Overlay.m_QuadDescSet, m_Overlay.m_QuadDescBufLayout,
                                      events, multiviewMask > 0);

        m_pDriver->ReplayLog(events.front(), events.back(), eReplay_Full);

        // resolve pass
        {
          cmd = m_pDriver->GetNextCmd();

          if(cmd == VK_NULL_HANDLE)
            return ResourceId();

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

          quadImBarrier.srcAccessMask = quadImBarrier.dstAccessMask;
          quadImBarrier.oldLayout = quadImBarrier.newLayout;

          quadImBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

          // wait for writing to finish
          DoPipelineBarrier(cmd, 1, &quadImBarrier);

          VkClearValue clearval = {};
          VkRenderPassBeginInfo rpbegin = {
              VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
              NULL,
              Unwrap(m_Overlay.NoDepthRP),
              Unwrap(m_Overlay.NoDepthFB),
              state.renderArea,
              1,
              &clearval,
          };
          vt->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

          if(multiviewMask > 0)
            vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                Unwrap(quadResolveMultiviewPipe));
          else
            vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                Unwrap(m_Overlay.m_QuadResolvePipeline[SampleIndex(iminfo.samples)]));
          vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                    Unwrap(m_Overlay.m_QuadResolvePipeLayout), 0, 1,
                                    UnwrapPtr(m_Overlay.m_QuadDescSet), 0, NULL);

          VkViewport viewport = {
              0.0f, 0.0f, (float)m_Overlay.ImageDim.width, (float)m_Overlay.ImageDim.height,
              0.0f, 1.0f};
          vt->CmdSetViewport(Unwrap(cmd), 0, 1, &viewport);

          vt->CmdDraw(Unwrap(cmd), 4, 1, 0, 0);
          vt->CmdEndRenderPass(Unwrap(cmd));

          vkr = vt->EndCommandBuffer(Unwrap(cmd));
          CHECK_VKR(m_pDriver, vkr);
        }

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

        m_pDriver->vkDestroyImageView(m_Device, quadImgView, NULL);
        m_pDriver->vkDestroyImage(m_Device, quadImg, NULL);
        m_pDriver->vkFreeMemory(m_Device, quadImgMem, NULL);
        m_pDriver->vkDestroyPipeline(m_Device, quadResolveMultiviewPipe, NULL);
      }

      // restore back to normal
      m_pDriver->ReplayLog(0, eventId, eReplay_WithoutDraw);

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

      vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
      CHECK_VKR(m_pDriver, vkr);
    }
  }
  else if(overlay == DebugOverlay::TriangleSizePass || overlay == DebugOverlay::TriangleSizeDraw)
  {
    if(!state.rastDiscardEnable && (multiviewMask == 0 || m_pDriver->MultiViewGeometryShaders()))
    {
      VulkanRenderState prevstate = state;

      const BuiltinShader pixelShaderVariant =
          multiviewMask > 0 ? BuiltinShader::TrisizeMultiviewFS : BuiltinShader::TrisizeFS;

      VkPipelineShaderStageCreateInfo stages[3] = {
          {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_VERTEX_BIT,
           shaderCache->GetBuiltinModule(BuiltinShader::MeshVS), "main", NULL},
          {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_FRAGMENT_BIT,
           shaderCache->GetBuiltinModule(pixelShaderVariant), "main", NULL},
          {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_GEOMETRY_BIT,
           shaderCache->GetBuiltinModule(BuiltinShader::TrisizeGS), "main", NULL},
      };

      if(stages[0].module != VK_NULL_HANDLE && stages[1].module != VK_NULL_HANDLE &&
         stages[2].module != VK_NULL_HANDLE)
      {
        SCOPED_TIMER("Triangle Size");

        float black[] = {0.0f, 0.0f, 0.0f, 0.0f};

        VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                                        NULL,
                                        VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
                                        VK_ACCESS_TRANSFER_WRITE_BIT,
                                        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                        VK_QUEUE_FAMILY_IGNORED,
                                        VK_QUEUE_FAMILY_IGNORED,
                                        Unwrap(m_Overlay.Image),
                                        subRange};

        DoPipelineBarrier(cmd, 1, &barrier);

        vt->CmdClearColorImage(Unwrap(cmd), Unwrap(m_Overlay.Image),
                               VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, (VkClearColorValue *)black, 1,
                               &subRange);

        std::swap(barrier.oldLayout, barrier.newLayout);
        std::swap(barrier.srcAccessMask, barrier.dstAccessMask);
        barrier.dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;

        DoPipelineBarrier(cmd, 1, &barrier);

        // end this cmd buffer so the image is in the right state for the next part
        vkr = vt->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);

        if(Vulkan_Debug_SingleSubmitFlushing())
          m_pDriver->SubmitCmds();

        rdcarray<uint32_t> events = passEvents;

        if(overlay == DebugOverlay::TriangleSizeDraw)
          events.clear();

        while(!events.empty())
        {
          const ActionDescription *action = m_pDriver->GetAction(events[0]);

          // remove any non-drawcalls, like the pass boundary.
          if(!action || !(action->flags & (ActionFlags::MeshDispatch | ActionFlags::Drawcall)))
            events.erase(0);
          else
            break;
        }

        events.push_back(eventId);

        m_pDriver->ReplayLog(0, events[0], eReplay_WithoutDraw);

        uint32_t meshOffs = 0;
        MeshUBOData *data = (MeshUBOData *)m_MeshRender.UBO.Map(&meshOffs);
        if(!data)
          return ResourceId();

        data->mvp = Matrix4f::Identity();
        data->invProj = Matrix4f::Identity();
        data->color = Vec4f();
        data->homogenousInput = 1;
        data->pointSpriteSize = Vec2f(0.0f, 0.0f);
        data->displayFormat = 0;
        data->rawoutput = 1;
        data->flipY = 0;
        data->vtxExploderSNorm = 0.0f;
        data->exploderScale = 0.0f;
        data->exploderCentre = Vec3f();
        m_MeshRender.UBO.Unmap();

        // second offset can vary per-view
        uint32_t offsets[2] = {meshOffs, 0};

        VkDescriptorBufferInfo bufdesc;
        m_MeshRender.UBO.FillDescriptor(bufdesc);

        VkWriteDescriptorSet write = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
                                      NULL,
                                      Unwrap(m_Overlay.m_TriSizeDescSet),
                                      0,
                                      0,
                                      1,
                                      VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
                                      NULL,
                                      &bufdesc,
                                      NULL};
        vt->UpdateDescriptorSets(Unwrap(m_Device), 1, &write, 0, NULL);

        m_Overlay.m_TriSizeUBO.FillDescriptor(bufdesc);
        write.dstBinding = 2;
        vt->UpdateDescriptorSets(Unwrap(m_Device), 1, &write, 0, NULL);

        VkRenderPass RP = m_Overlay.NoDepthRP;
        VkFramebuffer FB = m_Overlay.NoDepthFB;

        VulkanCreationInfo &createinfo = m_pDriver->m_CreationInfo;

        ResourceId depthStencilView;

        if(state.dynamicRendering.active)
        {
          depthStencilView = GetResID(state.dynamicRendering.depth.imageView);
          if(depthStencilView == ResourceId())
            depthStencilView = GetResID(state.dynamicRendering.stencil.imageView);
        }
        else
        {
          RDCASSERT(state.subpass < createinfo.m_RenderPass[state.GetRenderPass()].subpasses.size());
          int32_t dsIdx =
              createinfo.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].depthstencilAttachment;

          // make a renderpass and framebuffer for rendering to overlay color and using
          // depth buffer from the orignial render
          if(dsIdx >= 0 &&
             dsIdx < (int32_t)createinfo.m_Framebuffer[state.GetFramebuffer()].attachments.size())
          {
            depthStencilView = state.GetFramebufferAttachments()[dsIdx];
          }
        }

        if(depthStencilView != ResourceId())
        {
          VkAttachmentDescription attDescs[] = {
              {0, overlayFormat, VK_SAMPLE_COUNT_1_BIT, VK_ATTACHMENT_LOAD_OP_LOAD,
               VK_ATTACHMENT_STORE_OP_STORE, VK_ATTACHMENT_LOAD_OP_DONT_CARE,
               VK_ATTACHMENT_STORE_OP_DONT_CARE, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
               VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL},
              {0, VK_FORMAT_UNDEFINED, VK_SAMPLE_COUNT_1_BIT,    // will patch this just below
               VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_STORE, VK_ATTACHMENT_LOAD_OP_LOAD,
               VK_ATTACHMENT_STORE_OP_STORE, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
               VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL},
          };

          VulkanCreationInfo::ImageView &depthViewInfo = createinfo.m_ImageView[depthStencilView];

          ResourceId depthIm = depthViewInfo.image;
          VulkanCreationInfo::Image &depthImageInfo = createinfo.m_Image[depthIm];

          attDescs[1].format = depthImageInfo.format;
          attDescs[0].samples = attDescs[1].samples = iminfo.samples;

          {
            LockedConstImageStateRef imState = m_pDriver->FindConstImageState(depthIm);
            if(imState)
            {
              // find the state that overlaps the view's subresource range start. We assume all
              // subresources are correctly in the same state (as they should be) so we just need to
              // find the first match.
              auto it = imState->subresourceStates.RangeBegin(depthViewInfo.range);
              if(it != imState->subresourceStates.end())
                attDescs[1].initialLayout = attDescs[1].finalLayout = it->state().newLayout;
            }
          }

          VkAttachmentReference colRef = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
          VkAttachmentReference dsRef = {1, attDescs[1].initialLayout};

          VkSubpassDescription subp = {
              0,      VK_PIPELINE_BIND_POINT_GRAPHICS,
              0,      NULL,       // inputs
              1,      &colRef,    // color
              NULL,               // resolve
              &dsRef,             // depth-stencil
              0,      NULL,       // preserve
          };

          VkRenderPassCreateInfo rpinfo = {
              VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
              NULL,
              0,
              2,
              attDescs,
              1,
              &subp,
              0,
              NULL,    // dependencies
          };

          if(multiviewMask > 0)
            rpinfo.pNext = &multiviewRP;

          vkr = m_pDriver->vkCreateRenderPass(m_Device, &rpinfo, NULL, &RP);
          CHECK_VKR(m_pDriver, vkr);

          VkImageView views[] = {
              m_Overlay.ImageView,
              m_pDriver->GetResourceManager()->GetHandle<VkImageView>(depthStencilView),
          };

          // Create framebuffer rendering just to overlay image, no depth
          VkFramebufferCreateInfo fbinfo = {
              VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
              NULL,
              0,
              RP,
              2,
              views,
              RDCMAX(1U, m_Overlay.ImageDim.width >> sub.mip),
              RDCMAX(1U, m_Overlay.ImageDim.height >> sub.mip),
              sub.numSlices,
          };

          vkr = m_pDriver->vkCreateFramebuffer(m_Device, &fbinfo, NULL, &FB);
          CHECK_VKR(m_pDriver, vkr);
        }

        VkGraphicsPipelineCreateInfo pipeCreateInfo;

        m_pDriver->GetShaderCache()->MakeGraphicsPipelineInfo(pipeCreateInfo,
                                                              state.graphics.pipeline);

        VkPipelineInputAssemblyStateCreateInfo ia = {
            VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};

        // ia.topology will be set below on a per-draw basis

        VkVertexInputBindingDescription binds[] = {
            // primary
            {0, 0, VK_VERTEX_INPUT_RATE_VERTEX},
            // secondary
            {1, 0, VK_VERTEX_INPUT_RATE_VERTEX},
        };

        VkVertexInputAttributeDescription vertAttrs[] = {
            {0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0},
            {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0},
        };

        VkPipelineVertexInputStateCreateInfo vi = {
            VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
            NULL,
            0,
            1,
            binds,
            2,
            vertAttrs,
        };

        VkPipelineColorBlendAttachmentState attState = {
            false,
            VK_BLEND_FACTOR_ONE,
            VK_BLEND_FACTOR_ZERO,
            VK_BLEND_OP_ADD,
            VK_BLEND_FACTOR_ONE,
            VK_BLEND_FACTOR_ZERO,
            VK_BLEND_OP_ADD,
            0xf,
        };

        VkPipelineColorBlendStateCreateInfo cb = {
            VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
            NULL,
            0,
            false,
            VK_LOGIC_OP_NO_OP,
            1,
            &attState,
            {1.0f, 1.0f, 1.0f, 1.0f},
        };

        pipeCreateInfo.stageCount = 3;
        pipeCreateInfo.pStages = stages;
        pipeCreateInfo.pTessellationState = NULL;
        pipeCreateInfo.renderPass = RP;
        pipeCreateInfo.subpass = 0;
        pipeCreateInfo.layout = m_Overlay.m_TriSizePipeLayout;
        pipeCreateInfo.basePipelineHandle = VK_NULL_HANDLE;
        pipeCreateInfo.basePipelineIndex = 0;
        pipeCreateInfo.pInputAssemblyState = &ia;
        pipeCreateInfo.pVertexInputState = &vi;
        pipeCreateInfo.pColorBlendState = &cb;

        // don't use dynamic rendering
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO);
        // don't use custom resolve
        RemoveNextStruct(&pipeCreateInfo, VK_STRUCTURE_TYPE_CUSTOM_RESOLVE_CREATE_INFO_EXT);

        if(pipeCreateInfo.pDynamicState)
        {
          uint32_t &dynamicStateCount = (uint32_t &)pipeCreateInfo.pDynamicState->dynamicStateCount;
          VkDynamicState *dynamicStateList =
              (VkDynamicState *)pipeCreateInfo.pDynamicState->pDynamicStates;

          // remove any dynamic states we don't want
          for(uint32_t i = 0; i < dynamicStateCount;)
          {
            // we are controlling the vertex binding so we don't need the stride or input to be
            // dynamic.
            // Similarly we're controlling the topology so that doesn't need to be dynamic
            if(dynamicStateList[i] == VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE ||
               dynamicStateList[i] == VK_DYNAMIC_STATE_VERTEX_INPUT_EXT ||
               dynamicStateList[i] == VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY)
            {
              // swap with the last item if this isn't the last one
              if(i != dynamicStateCount - 1)
                std::swap(dynamicStateList[i], dynamicStateList[dynamicStateCount - 1]);

              // then pop the last item.
              dynamicStateCount--;

              // process this item again. If we swapped we'll then consider that dynamic state, and
              // if we didn't then this was the last item and i will be past dynamicStateCount now
              continue;
            }

            i++;
          }
        }

        typedef rdcpair<uint32_t, Topology> PipeKey;

        std::map<PipeKey, VkPipeline> pipes;

        // shader object vertex state
        VkVertexInputBindingDescription2EXT soBinds[2];
        VkVertexInputAttributeDescription2EXT soAttrs[2];

        VkShaderStageFlagBits stageFlags[3] = {
            VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT, VK_SHADER_STAGE_GEOMETRY_BIT};
        VkShaderEXT shaders[3] = {0};
        VkShaderEXT unwrappedShaders[3] = {0};

        if(state.graphics.shaderObject)
        {
          // create the tri-size shader objects
          const VulkanCreationInfo::PipelineLayout &layoutInfo =
              createinfo.m_PipelineLayout[GetResID(m_Overlay.m_TriSizePipeLayout)];

          rdcarray<VkDescriptorSetLayout> descSetLayouts;
          for(ResourceId setLayout : layoutInfo.descSetLayouts)
            descSetLayouts.push_back(
                m_pDriver->GetResourceManager()->GetHandle<VkDescriptorSetLayout>(setLayout));

          VkShaderCreateInfoEXT shadInfo = {
              VK_STRUCTURE_TYPE_SHADER_CREATE_INFO_EXT,
              NULL,
              0,
              VK_SHADER_STAGE_VERTEX_BIT,
              VK_SHADER_STAGE_GEOMETRY_BIT,
              VK_SHADER_CODE_TYPE_SPIRV_EXT,
              shaderCache->GetBuiltinBlob(BuiltinShader::MeshVS)->size() * sizeof(uint32_t),
              shaderCache->GetBuiltinBlob(BuiltinShader::MeshVS)->data(),
              "main",
              (uint32_t)descSetLayouts.size(),
              descSetLayouts.data(),
              (uint32_t)layoutInfo.pushRanges.size(),
              layoutInfo.pushRanges.data(),
              NULL};

          vkr = m_pDriver->vkCreateShadersEXT(m_Device, 1, &shadInfo, NULL, &shaders[0]);

          shadInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
          shadInfo.nextStage = 0;
          shadInfo.codeSize =
              shaderCache->GetBuiltinBlob(pixelShaderVariant)->size() * sizeof(uint32_t);
          shadInfo.pCode = shaderCache->GetBuiltinBlob(pixelShaderVariant)->data();

          vkr = m_pDriver->vkCreateShadersEXT(m_Device, 1, &shadInfo, NULL, &shaders[1]);

          shadInfo.stage = VK_SHADER_STAGE_GEOMETRY_BIT;
          shadInfo.nextStage = VK_SHADER_STAGE_FRAGMENT_BIT;
          shadInfo.codeSize =
              shaderCache->GetBuiltinBlob(BuiltinShader::TrisizeGS)->size() * sizeof(uint32_t);
          shadInfo.pCode = shaderCache->GetBuiltinBlob(BuiltinShader::TrisizeGS)->data();

          vkr = m_pDriver->vkCreateShadersEXT(m_Device, 1, &shadInfo, NULL, &shaders[2]);

          // vertex state
          // primary
          soBinds[0] = {VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT,
                        0,
                        0,
                        0,
                        VK_VERTEX_INPUT_RATE_VERTEX,
                        1};
          // secondary
          soBinds[1] = {VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT,
                        0,
                        1,
                        0,
                        VK_VERTEX_INPUT_RATE_VERTEX,
                        1};

          soAttrs[0] = {VK_STRUCTURE_TYPE_VERTEX_INPUT_ATTRIBUTE_DESCRIPTION_2_EXT,
                        0,
                        0,
                        0,
                        VK_FORMAT_R32G32B32A32_SFLOAT,
                        0};
          soAttrs[1] = {VK_STRUCTURE_TYPE_VERTEX_INPUT_ATTRIBUTE_DESCRIPTION_2_EXT,
                        0,
                        1,
                        0,
                        VK_FORMAT_R32G32B32A32_SFLOAT,
                        0};

          unwrappedShaders[0] = Unwrap(shaders[0]);
          unwrappedShaders[1] = Unwrap(shaders[1]);
          unwrappedShaders[2] = Unwrap(shaders[2]);
        }

        uint32_t viewOffset[32] = {};

        uint32_t firstView = 0;
        uint32_t lastView = 0;

        if(multiviewMask > 0)
        {
          firstView = Bits::CountTrailingZeroes(multiviewMask);
          lastView = 31 - Bits::CountLeadingZeroes(multiviewMask);
        }

        for(size_t i = 0; i < events.size(); i++)
        {
          for(uint32_t view = firstView; view <= lastView; view++)
          {
            Vec4f *ubo = (Vec4f *)m_Overlay.m_TriSizeUBO.Map(&viewOffset[view]);
            if(!ubo)
              return ResourceId();

            if(viewportPerView)
            {
              *ubo = Vec4f(state.views[view].width, state.views[view].height, 0.0f, 0.0f);
            }
            else
            {
              *ubo = Vec4f(state.views[0].width, state.views[0].height, 0.0f, 0.0f);
            }

            m_Overlay.m_TriSizeUBO.Unmap();
          }

          cmd = m_pDriver->GetNextCmd();

          if(cmd == VK_NULL_HANDLE)
            return ResourceId();

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

          VkClearValue clearval = {};
          VkRenderPassBeginInfo rpbegin = {
              VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
              NULL,
              Unwrap(RP),
              Unwrap(FB),
              {
                  {0, 0},
                  {
                      RDCMAX(1U, m_Overlay.ImageDim.width >> sub.mip),
                      RDCMAX(1U, m_Overlay.ImageDim.height >> sub.mip),
                  },
              },
              1,
              &clearval,
          };
          vt->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

          const ActionDescription *action = m_pDriver->GetAction(events[i]);

          for(uint32_t view = firstView; view <= lastView; view++)
          {
            // skip if this view isn't in the multiview mask (if we have one)
            if(multiviewMask > 0 && (multiviewMask & (1 << view)) == 0)
              continue;

            offsets[1] = viewOffset[view];

            for(uint32_t inst = 0; action && inst < RDCMAX(1U, action->numInstances); inst++)
            {
              MeshFormat fmt = GetPostVSBuffers(events[i], inst, view, MeshDataStage::GSOut);
              if(fmt.vertexResourceId == ResourceId())
                fmt = GetPostVSBuffers(events[i], inst, view, MeshDataStage::VSOut);
              if(fmt.vertexResourceId == ResourceId())
                fmt = GetPostVSBuffers(events[i], inst, view, MeshDataStage::MeshOut);

              if(fmt.vertexResourceId != ResourceId())
              {
                ia.topology = MakeVkPrimitiveTopology(fmt.topology);

                binds[0].stride = binds[1].stride = fmt.vertexByteStride;
                soBinds[0].stride = soBinds[1].stride = fmt.vertexByteStride;

                PipeKey key = make_rdcpair(fmt.vertexByteStride, fmt.topology);
                VkPipeline pipe = pipes[key];

                if(pipe == VK_NULL_HANDLE && !state.graphics.shaderObject)
                {
                  vkr = m_pDriver->vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1,
                                                             &pipeCreateInfo, NULL, &pipe);
                  CHECK_VKR(m_pDriver, vkr);
                }

                VkBuffer vb =
                    m_pDriver->GetResourceManager()->GetHandle<VkBuffer>(fmt.vertexResourceId);

                VkDeviceSize offs = fmt.vertexByteOffset;
                vt->CmdBindVertexBuffers(Unwrap(cmd), 0, 1, UnwrapPtr(vb), &offs);

                pipes[key] = pipe;

                vt->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                          Unwrap(m_Overlay.m_TriSizePipeLayout), 0, 1,
                                          UnwrapPtr(m_Overlay.m_TriSizeDescSet), 2, offsets);

                if(state.graphics.shaderObject)
                  vt->CmdBindShadersEXT(Unwrap(cmd), 3, stageFlags, unwrappedShaders);
                else
                  vt->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS, Unwrap(pipe));

                vt->CmdPushConstants(Unwrap(cmd), Unwrap(m_Overlay.m_TriSizePipeLayout),
                                     VK_SHADER_STAGE_ALL, 0, 4, &view);

                const VkPipelineDynamicStateCreateInfo *dyn = pipeCreateInfo.pDynamicState;

                for(uint32_t dynState = 0; dyn && dynState < dyn->dynamicStateCount; dynState++)
                {
                  VkDynamicState d = dyn->pDynamicStates[dynState];

                  if(!state.views.empty() && d == VK_DYNAMIC_STATE_VIEWPORT)
                  {
                    if(viewportPerView)
                    {
                      vt->CmdSetViewport(Unwrap(cmd), 0, 1, &state.views[view]);
                    }
                    else
                    {
                      vt->CmdSetViewport(Unwrap(cmd), 0, (uint32_t)state.views.size(),
                                         &state.views[0]);
                    }
                  }
                  else if(!state.scissors.empty() && d == VK_DYNAMIC_STATE_SCISSOR)
                  {
                    if(viewportPerView && view < state.scissors.size())
                    {
                      vt->CmdSetScissor(Unwrap(cmd), 0, 1, &state.scissors[view]);
                    }
                    else
                    {
                      vt->CmdSetScissor(Unwrap(cmd), 0, (uint32_t)state.scissors.size(),
                                        &state.scissors[0]);
                    }
                  }
                  else if(d == VK_DYNAMIC_STATE_LINE_WIDTH)
                  {
                    vt->CmdSetLineWidth(Unwrap(cmd), state.lineWidth);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_BIAS)
                  {
                    vt->CmdSetDepthBias(Unwrap(cmd), state.bias.depth, state.bias.biasclamp,
                                        state.bias.slope);
                  }
                  else if(d == VK_DYNAMIC_STATE_BLEND_CONSTANTS)
                  {
                    vt->CmdSetBlendConstants(Unwrap(cmd), state.blendConst);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_BOUNDS)
                  {
                    vt->CmdSetDepthBounds(Unwrap(cmd), state.mindepth, state.maxdepth);
                  }
                  else if(d == VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK)
                  {
                    vt->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                                 state.back.compare);
                    vt->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                                 state.front.compare);
                  }
                  else if(d == VK_DYNAMIC_STATE_STENCIL_WRITE_MASK)
                  {
                    vt->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                               state.back.write);
                    vt->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                               state.front.write);
                  }
                  else if(d == VK_DYNAMIC_STATE_STENCIL_REFERENCE)
                  {
                    vt->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT, state.back.ref);
                    vt->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                               state.front.ref);
                  }
                  else if(d == VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT)
                  {
                    vt->CmdSetViewportWithCountEXT(Unwrap(cmd), (uint32_t)state.views.size(),
                                                   state.views.data());
                  }
                  else if(d == VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT)
                  {
                    vt->CmdSetScissorWithCountEXT(Unwrap(cmd), (uint32_t)state.scissors.size(),
                                                  state.scissors.data());
                  }
                  else if(d == VK_DYNAMIC_STATE_CULL_MODE)
                  {
                    vt->CmdSetCullModeEXT(Unwrap(cmd), state.cullMode);
                  }
                  else if(d == VK_DYNAMIC_STATE_FRONT_FACE)
                  {
                    vt->CmdSetFrontFaceEXT(Unwrap(cmd), state.frontFace);
                  }
                  else if(d == VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY)
                  {
                    RDCERR("Primitive topology dynamic state found, should have been stripped");
                  }
                  else if(d == VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE)
                  {
                    RDCERR(
                        "Vertex input binding stride dynamic state found, should have been "
                        "stripped");
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE)
                  {
                    vt->CmdSetDepthTestEnableEXT(Unwrap(cmd), state.depthTestEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE)
                  {
                    vt->CmdSetDepthWriteEnableEXT(Unwrap(cmd), state.depthWriteEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_COMPARE_OP)
                  {
                    vt->CmdSetDepthCompareOpEXT(Unwrap(cmd), state.depthCompareOp);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE)
                  {
                    vt->CmdSetDepthBoundsTestEnableEXT(Unwrap(cmd), state.depthBoundsTestEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE)
                  {
                    vt->CmdSetStencilTestEnableEXT(Unwrap(cmd), state.stencilTestEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_STENCIL_OP)
                  {
                    vt->CmdSetStencilOpEXT(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                           state.front.failOp, state.front.passOp,
                                           state.front.depthFailOp, state.front.compareOp);
                    vt->CmdSetStencilOpEXT(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                           state.front.failOp, state.front.passOp,
                                           state.front.depthFailOp, state.front.compareOp);
                  }
                  else if(d == VK_DYNAMIC_STATE_COLOR_WRITE_ENABLE_EXT)
                  {
                    vt->CmdSetColorWriteEnableEXT(Unwrap(cmd),
                                                  (uint32_t)state.colorWriteEnable.size(),
                                                  state.colorWriteEnable.data());
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE)
                  {
                    vt->CmdSetDepthBiasEnableEXT(Unwrap(cmd), state.depthBiasEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_LOGIC_OP_EXT)
                  {
                    vt->CmdSetLogicOpEXT(Unwrap(cmd), state.logicOp);
                  }
                  else if(d == VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT)
                  {
                    vt->CmdSetPatchControlPointsEXT(Unwrap(cmd), state.patchControlPoints);
                  }
                  else if(d == VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE)
                  {
                    vt->CmdSetPrimitiveRestartEnableEXT(Unwrap(cmd), state.primRestartEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE)
                  {
                    vt->CmdSetRasterizerDiscardEnableEXT(Unwrap(cmd), state.rastDiscardEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_VERTEX_INPUT_EXT)
                  {
                    RDCERR("Vertex input dynamic state found, should have been stripped");
                  }
                  else if(d == VK_DYNAMIC_STATE_ATTACHMENT_FEEDBACK_LOOP_ENABLE_EXT)
                  {
                    vt->CmdSetAttachmentFeedbackLoopEnableEXT(Unwrap(cmd), state.feedbackAspects);
                  }
                  else if(d == VK_DYNAMIC_STATE_ALPHA_TO_COVERAGE_ENABLE_EXT)
                  {
                    vt->CmdSetAlphaToCoverageEnableEXT(Unwrap(cmd), state.alphaToCoverageEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_ALPHA_TO_ONE_ENABLE_EXT)
                  {
                    vt->CmdSetAlphaToOneEnableEXT(Unwrap(cmd), state.alphaToOneEnable);
                  }
                  else if(!state.colorBlendEnable.empty() &&
                          d == VK_DYNAMIC_STATE_COLOR_BLEND_ENABLE_EXT)
                  {
                    vt->CmdSetColorBlendEnableEXT(Unwrap(cmd), 0,
                                                  (uint32_t)state.colorBlendEnable.size(),
                                                  state.colorBlendEnable.data());
                  }
                  else if(!state.colorBlendEquation.empty() &&
                          d == VK_DYNAMIC_STATE_COLOR_BLEND_EQUATION_EXT)
                  {
                    vt->CmdSetColorBlendEquationEXT(Unwrap(cmd), 0,
                                                    (uint32_t)state.colorBlendEquation.size(),
                                                    state.colorBlendEquation.data());
                  }
                  else if(!state.colorWriteMask.empty() && d == VK_DYNAMIC_STATE_COLOR_WRITE_MASK_EXT)
                  {
                    vt->CmdSetColorWriteMaskEXT(Unwrap(cmd), 0, (uint32_t)state.colorWriteMask.size(),
                                                state.colorWriteMask.data());
                  }
                  else if(d == VK_DYNAMIC_STATE_CONSERVATIVE_RASTERIZATION_MODE_EXT)
                  {
                    vt->CmdSetConservativeRasterizationModeEXT(Unwrap(cmd),
                                                               state.conservativeRastMode);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_CLAMP_ENABLE_EXT)
                  {
                    vt->CmdSetDepthClampEnableEXT(Unwrap(cmd), state.depthClampEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_CLIP_ENABLE_EXT)
                  {
                    vt->CmdSetDepthClipEnableEXT(Unwrap(cmd), state.depthClipEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_DEPTH_CLIP_NEGATIVE_ONE_TO_ONE_EXT)
                  {
                    vt->CmdSetDepthClipNegativeOneToOneEXT(Unwrap(cmd), state.negativeOneToOne);
                  }
                  else if(d == VK_DYNAMIC_STATE_EXTRA_PRIMITIVE_OVERESTIMATION_SIZE_EXT)
                  {
                    vt->CmdSetExtraPrimitiveOverestimationSizeEXT(Unwrap(cmd),
                                                                  state.primOverestimationSize);
                  }
                  else if(d == VK_DYNAMIC_STATE_LINE_RASTERIZATION_MODE_EXT)
                  {
                    vt->CmdSetLineRasterizationModeEXT(Unwrap(cmd), state.lineRasterMode);
                  }
                  else if(d == VK_DYNAMIC_STATE_LINE_STIPPLE_ENABLE_EXT)
                  {
                    vt->CmdSetLineStippleEnableEXT(Unwrap(cmd), state.stippledLineEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_LOGIC_OP_ENABLE_EXT)
                  {
                    vt->CmdSetLogicOpEnableEXT(Unwrap(cmd), state.logicOpEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_POLYGON_MODE_EXT)
                  {
                    vt->CmdSetPolygonModeEXT(Unwrap(cmd), state.polygonMode);
                  }
                  else if(d == VK_DYNAMIC_STATE_PROVOKING_VERTEX_MODE_EXT)
                  {
                    vt->CmdSetProvokingVertexModeEXT(Unwrap(cmd), state.provokingVertexMode);
                  }
                  else if(d == VK_DYNAMIC_STATE_RASTERIZATION_SAMPLES_EXT)
                  {
                    vt->CmdSetRasterizationSamplesEXT(Unwrap(cmd), state.rastSamples);
                  }
                  else if(d == VK_DYNAMIC_STATE_RASTERIZATION_STREAM_EXT)
                  {
                    vt->CmdSetRasterizationStreamEXT(Unwrap(cmd), state.rasterStream);
                  }
                  else if(d == VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_ENABLE_EXT)
                  {
                    vt->CmdSetSampleLocationsEnableEXT(Unwrap(cmd), state.sampleLocEnable);
                  }
                  else if(d == VK_DYNAMIC_STATE_SAMPLE_MASK_EXT)
                  {
                    vt->CmdSetSampleMaskEXT(Unwrap(cmd), state.rastSamples, state.sampleMask.data());
                  }
                  else if(d == VK_DYNAMIC_STATE_TESSELLATION_DOMAIN_ORIGIN_EXT)
                  {
                    vt->CmdSetTessellationDomainOriginEXT(Unwrap(cmd), state.domainOrigin);
                  }
                }

                if(state.graphics.shaderObject)
                {
                  if(!state.views.empty() && state.dynamicStates[VkDynamicViewport])
                  {
                    vt->CmdSetViewport(Unwrap(cmd), 0, (uint32_t)state.views.size(), &state.views[0]);
                  }
                  if(!state.scissors.empty() && state.dynamicStates[VkDynamicScissor])
                  {
                    vt->CmdSetScissor(Unwrap(cmd), 0, (uint32_t)state.scissors.size(),
                                      &state.scissors[0]);
                  }
                  if(state.dynamicStates[VkDynamicLineWidth])
                  {
                    vt->CmdSetLineWidth(Unwrap(cmd), state.lineWidth);
                  }
                  if(state.dynamicStates[VkDynamicDepthBias])
                  {
                    vt->CmdSetDepthBias(Unwrap(cmd), state.bias.depth, state.bias.biasclamp,
                                        state.bias.slope);
                  }
                  if(state.dynamicStates[VkDynamicBlendConstants])
                  {
                    vt->CmdSetBlendConstants(Unwrap(cmd), state.blendConst);
                  }
                  if(state.dynamicStates[VkDynamicDepthBounds])
                  {
                    vt->CmdSetDepthBounds(Unwrap(cmd), state.mindepth, state.maxdepth);
                  }
                  if(state.dynamicStates[VkDynamicStencilCompareMask])
                  {
                    vt->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                                 state.back.compare);
                    vt->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                                 state.front.compare);
                  }
                  if(state.dynamicStates[VkDynamicStencilWriteMask])
                  {
                    vt->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                               state.back.write);
                    vt->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                               state.front.write);
                  }
                  if(state.dynamicStates[VkDynamicStencilReference])
                  {
                    vt->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT, state.back.ref);
                    vt->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                               state.front.ref);
                  }
                  if(state.dynamicStates[VkDynamicViewportCount])
                  {
                    vt->CmdSetViewportWithCountEXT(Unwrap(cmd), (uint32_t)state.views.size(),
                                                   state.views.data());
                  }
                  if(state.dynamicStates[VkDynamicScissorCount])
                  {
                    vt->CmdSetScissorWithCountEXT(Unwrap(cmd), (uint32_t)state.scissors.size(),
                                                  state.scissors.data());
                  }
                  if(state.dynamicStates[VkDynamicCullMode])
                  {
                    vt->CmdSetCullModeEXT(Unwrap(cmd), state.cullMode);
                  }
                  if(state.dynamicStates[VkDynamicFrontFace])
                  {
                    vt->CmdSetFrontFaceEXT(Unwrap(cmd), state.frontFace);
                  }

                  // overriding topology
                  vt->CmdSetPrimitiveTopologyEXT(Unwrap(cmd), MakeVkPrimitiveTopology(fmt.topology));

                  // VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE unnecessary since utilizing vertex input

                  if(state.dynamicStates[VkDynamicDepthTestEnable])
                  {
                    vt->CmdSetDepthTestEnableEXT(Unwrap(cmd), state.depthTestEnable);
                  }
                  if(state.dynamicStates[VkDynamicDepthWriteEnable])
                  {
                    vt->CmdSetDepthWriteEnableEXT(Unwrap(cmd), state.depthWriteEnable);
                  }
                  if(state.dynamicStates[VkDynamicDepthCompareOp])
                  {
                    vt->CmdSetDepthCompareOpEXT(Unwrap(cmd), state.depthCompareOp);
                  }
                  if(state.dynamicStates[VkDynamicDepthBoundsTestEnable])
                  {
                    vt->CmdSetDepthBoundsTestEnableEXT(Unwrap(cmd), state.depthBoundsTestEnable);
                  }
                  if(state.dynamicStates[VkDynamicStencilTestEnable])
                  {
                    vt->CmdSetStencilTestEnableEXT(Unwrap(cmd), state.stencilTestEnable);
                  }
                  if(state.dynamicStates[VkDynamicStencilOp])
                  {
                    vt->CmdSetStencilOpEXT(Unwrap(cmd), VK_STENCIL_FACE_FRONT_BIT,
                                           state.front.failOp, state.front.passOp,
                                           state.front.depthFailOp, state.front.compareOp);
                    vt->CmdSetStencilOpEXT(Unwrap(cmd), VK_STENCIL_FACE_BACK_BIT,
                                           state.front.failOp, state.front.passOp,
                                           state.front.depthFailOp, state.front.compareOp);
                  }
                  if(!state.colorWriteEnable.empty() && state.dynamicStates[VkDynamicColorWriteEXT])
                  {
                    vt->CmdSetColorWriteEnableEXT(Unwrap(cmd),
                                                  (uint32_t)state.colorWriteEnable.size(),
                                                  state.colorWriteEnable.data());
                  }
                  if(state.dynamicStates[VkDynamicDepthBiasEnable])
                  {
                    vt->CmdSetDepthBiasEnableEXT(Unwrap(cmd), state.depthBiasEnable);
                  }
                  if(state.dynamicStates[VkDynamicLogicOpEXT])
                  {
                    vt->CmdSetLogicOpEXT(Unwrap(cmd), state.logicOp);
                  }
                  if(state.dynamicStates[VkDynamicControlPointsEXT])
                  {
                    vt->CmdSetPatchControlPointsEXT(Unwrap(cmd), state.patchControlPoints);
                  }
                  if(state.dynamicStates[VkDynamicPrimRestart])
                  {
                    vt->CmdSetPrimitiveRestartEnableEXT(Unwrap(cmd), state.primRestartEnable);
                  }
                  if(state.dynamicStates[VkDynamicRastDiscard])
                  {
                    vt->CmdSetRasterizerDiscardEnableEXT(Unwrap(cmd), state.rastDiscardEnable);
                  }

                  // overriding vertex input
                  vt->CmdSetVertexInputEXT(Unwrap(cmd), 2, soBinds, 2, soAttrs);

                  if(state.dynamicStates[VkDynamicAttachmentFeedbackLoopEnableEXT])
                  {
                    vt->CmdSetAttachmentFeedbackLoopEnableEXT(Unwrap(cmd), state.feedbackAspects);
                  }
                  if(state.dynamicStates[VkDynamicAlphaToCoverageEXT])
                  {
                    vt->CmdSetAlphaToCoverageEnableEXT(Unwrap(cmd), state.alphaToCoverageEnable);
                  }
                  if(state.dynamicStates[VkDynamicAlphaToOneEXT])
                  {
                    vt->CmdSetAlphaToOneEnableEXT(Unwrap(cmd), state.alphaToOneEnable);
                  }
                  if(!state.colorBlendEnable.empty() &&
                     state.dynamicStates[VkDynamicColorBlendEnableEXT])
                  {
                    vt->CmdSetColorBlendEnableEXT(Unwrap(cmd), 0,
                                                  (uint32_t)state.colorBlendEnable.size(),
                                                  state.colorBlendEnable.data());
                  }
                  if(!state.colorBlendEquation.empty() &&
                     state.dynamicStates[VkDynamicColorBlendEquationEXT])
                  {
                    vt->CmdSetColorBlendEquationEXT(Unwrap(cmd), 0,
                                                    (uint32_t)state.colorBlendEquation.size(),
                                                    state.colorBlendEquation.data());
                  }
                  if(!state.colorWriteMask.empty() && state.dynamicStates[VkDynamicColorWriteMaskEXT])
                  {
                    vt->CmdSetColorWriteMaskEXT(Unwrap(cmd), 0, (uint32_t)state.colorWriteMask.size(),
                                                state.colorWriteMask.data());
                  }
                  if(state.dynamicStates[VkDynamicConservativeRastModeEXT])
                  {
                    vt->CmdSetConservativeRasterizationModeEXT(Unwrap(cmd),
                                                               state.conservativeRastMode);
                  }
                  if(state.dynamicStates[VkDynamicDepthClampEnableEXT])
                  {
                    vt->CmdSetDepthClampEnableEXT(Unwrap(cmd), state.depthClampEnable);
                  }
                  if(state.dynamicStates[VkDynamicDepthClipEnableEXT])
                  {
                    vt->CmdSetDepthClipEnableEXT(Unwrap(cmd), state.depthClipEnable);
                  }
                  if(state.dynamicStates[VkDynamicDepthClipNegativeOneEXT])
                  {
                    vt->CmdSetDepthClipNegativeOneToOneEXT(Unwrap(cmd), state.negativeOneToOne);
                  }
                  if(state.dynamicStates[VkDynamicOverstimationSizeEXT])
                  {
                    vt->CmdSetExtraPrimitiveOverestimationSizeEXT(Unwrap(cmd),
                                                                  state.primOverestimationSize);
                  }
                  if(state.dynamicStates[VkDynamicLineRastModeEXT])
                  {
                    vt->CmdSetLineRasterizationModeEXT(Unwrap(cmd), state.lineRasterMode);
                  }
                  if(state.dynamicStates[VkDynamicLineStippleEnableEXT])
                  {
                    vt->CmdSetLineStippleEnableEXT(Unwrap(cmd), state.stippledLineEnable);
                  }
                  if(state.dynamicStates[VkDynamicLogicOpEnableEXT])
                  {
                    vt->CmdSetLogicOpEnableEXT(Unwrap(cmd), state.logicOpEnable);
                  }
                  if(state.dynamicStates[VkDynamicPolygonModeEXT])
                  {
                    vt->CmdSetPolygonModeEXT(Unwrap(cmd), state.polygonMode);
                  }
                  if(state.dynamicStates[VkDynamicProvokingVertexModeEXT])
                  {
                    vt->CmdSetProvokingVertexModeEXT(Unwrap(cmd), state.provokingVertexMode);
                  }
                  if(state.dynamicStates[VkDynamicRasterizationSamplesEXT])
                  {
                    vt->CmdSetRasterizationSamplesEXT(Unwrap(cmd), state.rastSamples);
                  }
                  if(state.dynamicStates[VkDynamicRasterizationStreamEXT])
                  {
                    vt->CmdSetRasterizationStreamEXT(Unwrap(cmd), state.rasterStream);
                  }
                  if(state.dynamicStates[VkDynamicSampleLocationsEnableEXT])
                  {
                    vt->CmdSetSampleLocationsEnableEXT(Unwrap(cmd), state.sampleLocEnable);
                  }
                  if(state.dynamicStates[VkDynamicSampleMaskEXT])
                  {
                    vt->CmdSetSampleMaskEXT(Unwrap(cmd), state.rastSamples, state.sampleMask.data());
                  }
                  if(state.dynamicStates[VkDynamicTessDomainOriginEXT])
                  {
                    vt->CmdSetTessellationDomainOriginEXT(Unwrap(cmd), state.domainOrigin);
                  }
                }

                if(fmt.indexByteStride)
                {
                  VkIndexType idxtype = VK_INDEX_TYPE_UINT16;
                  if(fmt.indexByteStride == 4)
                    idxtype = VK_INDEX_TYPE_UINT32;
                  else if(fmt.indexByteStride == 1)
                    idxtype = VK_INDEX_TYPE_UINT8;

                  if(fmt.indexResourceId != ResourceId())
                  {
                    VkBuffer ib =
                        m_pDriver->GetResourceManager()->GetHandle<VkBuffer>(fmt.indexResourceId);

                    vt->CmdBindIndexBuffer(Unwrap(cmd), Unwrap(ib), fmt.indexByteOffset, idxtype);
                    vt->CmdDrawIndexed(Unwrap(cmd), fmt.numIndices, 1, 0, fmt.baseVertex, 0);
                  }
                }
                else
                {
                  vt->CmdDraw(Unwrap(cmd), fmt.numIndices, 1, 0, 0);
                }
              }
            }
          }

          vt->CmdEndRenderPass(Unwrap(cmd));

          vkr = vt->EndCommandBuffer(Unwrap(cmd));
          CHECK_VKR(m_pDriver, vkr);

          if(overlay == DebugOverlay::TriangleSizePass)
          {
            m_pDriver->ReplayLog(events[i], events[i], eReplay_OnlyDraw);

            if(i + 1 < events.size())
              m_pDriver->ReplayLog(events[i], events[i + 1], eReplay_WithoutDraw);
          }
        }

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

        if(depthStencilView != ResourceId())
        {
          m_pDriver->vkDestroyFramebuffer(m_Device, FB, NULL);
          m_pDriver->vkDestroyRenderPass(m_Device, RP, NULL);
        }

        for(auto it = pipes.begin(); it != pipes.end(); ++it)
          m_pDriver->vkDestroyPipeline(m_Device, it->second, NULL);

        for(uint32_t i = 0; i < 3; i++)
        {
          if(shaders[i] != VK_NULL_HANDLE)
            m_pDriver->vkDestroyShaderEXT(m_Device, shaders[i], NULL);
        }
      }
      else
      {
        vkr = vt->EndCommandBuffer(Unwrap(cmd));
        CHECK_VKR(m_pDriver, vkr);
      }

      // restore back to normal
      m_pDriver->ReplayLog(0, eventId, eReplay_WithoutDraw);

      // restore state
      state = prevstate;

      cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return ResourceId();

      vkr = vt->BeginCommandBuffer(Unwrap(cmd), &beginInfo);
      CHECK_VKR(m_pDriver, vkr);
    }
  }

  VkMarkerRegion::End(cmd);

  vkr = vt->EndCommandBuffer(Unwrap(cmd));
  CHECK_VKR(m_pDriver, vkr);

  if(Vulkan_Debug_SingleSubmitFlushing())
    m_pDriver->SubmitCmds();

  return GetResID(m_Overlay.Image);
}
