/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2020-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 "core/settings.h"
#include "driver/shaders/spirv/spirv_debug.h"
#include "driver/shaders/spirv/spirv_editor.h"
#include "driver/shaders/spirv/spirv_op_helpers.h"
#include "maths/formatpacking.h"
#include "replay/common/var_dispatch_helpers.h"
#include "vk_core.h"
#include "vk_debug.h"
#include "vk_replay.h"
#include "vk_shader_cache.h"

#undef None

RDOC_CONFIG(rdcstr, Vulkan_Debug_PSDebugDumpDirPath, "",
            "Path to dump shader debugging generated SPIR-V files.");
RDOC_CONFIG(bool, Vulkan_Debug_ShaderDebugLogging, false,
            "Output verbose debug logging messages when debugging shaders.");

RDOC_CONFIG(bool, Vulkan_Debug_EnableShaderDebugMT, true,
            "Use multiple threads to run the shader debugger simulation.");

// needed for old linux compilers
namespace std
{
template <>
struct hash<ShaderBuiltin>
{
  std::size_t operator()(const ShaderBuiltin &e) const { return size_t(e); }
};
}

// should match the descriptor set layout created in ShaderDebugData::Init()
enum class ShaderDebugBind
{
  Tex1D = 1,
  First = Tex1D,
  Tex2D = 2,
  Tex3D = 3,
  Tex2DMS = 4,
  TexCube = 5,
  Buffer = 6,
  Sampler = 7,
  Constants = 8,
  Count,
  MathResult = 9,
};

struct GatherOffsets
{
  int32_t u0, v0, u1, v1, u2, v2, u3, v3;
};

struct ShaderConstParameters
{
  uint32_t operation;
  VkBool32 useGradOrGatherOffsets;
  ShaderDebugBind dim;
  rdcspv::GatherChannel gatherChannel;
  union
  {
    GatherOffsets gatherOffsets;
    Vec3i constOffsets;
  };

  uint32_t hashKey(uint32_t shaderIndex) const
  {
    uint32_t hash = 5381;
    hash = ((hash << 5) + hash) + shaderIndex;
    hash = ((hash << 5) + hash) + operation;
    hash = ((hash << 5) + hash) + useGradOrGatherOffsets;
    hash = ((hash << 5) + hash) + (uint32_t)dim;
    hash = ((hash << 5) + hash) + (uint32_t)gatherChannel;
    hash = ((hash << 5) + hash) + gatherOffsets.u0;
    hash = ((hash << 5) + hash) + gatherOffsets.v0;
    hash = ((hash << 5) + hash) + gatherOffsets.u1;
    hash = ((hash << 5) + hash) + gatherOffsets.v1;
    hash = ((hash << 5) + hash) + gatherOffsets.u2;
    hash = ((hash << 5) + hash) + gatherOffsets.v2;
    hash = ((hash << 5) + hash) + gatherOffsets.u3;
    hash = ((hash << 5) + hash) + gatherOffsets.v3;
    return hash;
  }
};

struct ShaderUniformParameters
{
  Vec3i texel_uvw;
  int texel_lod;
  float uvwa[4];
  float ddx[3];
  float ddy[3];
  Vec3i offset;
  int sampleIdx;
  float compare;
  float lod;
  float minlod;
};

#if ENABLED(RDOC_RELEASE)
#define CHECK_DEVICE_THREAD()
#else
#define CHECK_DEVICE_THREAD() \
  RDCASSERTMSG("API Wrapper function called from non-device thread!", IsDeviceThread());
#endif    // #if ENABLED(RDOC_RELEASE)

class VulkanAPIWrapper : public rdcspv::DebugAPIWrapper
{
public:
  VulkanAPIWrapper(WrappedVulkan *vk, VulkanCreationInfo &creation, ShaderStage stage, uint32_t eid,
                   ResourceId shadId)
      : m_DebugData(vk->GetReplay()->GetShaderDebugData()),
        m_Creation(creation),
        m_EventID(eid),
        m_ShaderID(shadId),
        deviceThreadID(Threading::GetCurrentID())
  {
    m_pDriver = vk;

    // when we're first setting up, the state is pristine and no replay is needed
    m_ResourcesDirty = false;

    VulkanReplay *replay = m_pDriver->GetReplay();

    // cache the descriptor access. This should be a superset of all descriptors we need to read from
    m_Access = replay->GetDescriptorAccess(eid);

    // filter to only accesses from the stage we care about, as access lookups will be stage-specific
    m_Access.removeIf([stage](const DescriptorAccess &access) { return access.stage != stage; });

    // fetch all descriptor contents now too
    m_Descriptors.reserve(m_Access.size());
    m_SamplerDescriptors.reserve(m_Access.size());

    // we could collate ranges by descriptor store, but in practice we don't expect descriptors to
    // be scattered across multiple stores. So to keep the code simple for now we do a linear sweep
    ResourceId store;
    rdcarray<DescriptorRange> ranges;

    for(const DescriptorAccess &acc : m_Access)
    {
      if(acc.descriptorStore != store)
      {
        if(store != ResourceId())
        {
          m_Descriptors.append(replay->GetDescriptors(store, ranges));
          m_SamplerDescriptors.append(replay->GetSamplerDescriptors(store, ranges));
        }

        store = acc.descriptorStore;
        ranges.clear();
      }

      // if the last range is contiguous with this access, append this access as a new range to query
      if(!ranges.empty() && ranges.back().descriptorSize == acc.byteSize &&
         ranges.back().offset + ranges.back().descriptorSize == acc.byteOffset &&
         ranges.back().type == acc.type)
      {
        ranges.back().count++;
        continue;
      }

      DescriptorRange range = acc;
      ranges.push_back(range);
    }

    if(store != ResourceId())
    {
      m_Descriptors.append(replay->GetDescriptors(store, ranges));
      m_SamplerDescriptors.append(replay->GetSamplerDescriptors(store, ranges));
    }

    // apply dynamic offsets to our cached descriptors
    // we iterate over descriptors first to find dynamic ones, then iterate over our cached set to
    // apply. Neither array should be large but there should be fewer dynamic descriptors in total
    {
      const VulkanRenderState &state = m_pDriver->GetRenderState();

      const rdcarray<VulkanStatePipeline::DescriptorAndOffsets> &src =
          (stage == ShaderStage::Compute ? state.compute.descSets : state.graphics.descSets);

      for(size_t i = 0; i < src.size(); i++)
      {
        const VulkanStatePipeline::DescriptorAndOffsets &srcData = src[i];
        ResourceId sourceSet = srcData.descSet;
        const uint32_t *srcOffset = srcData.offsets.begin();

        // this could be either an unbound set, or descriptor buffers (which can't use dynamic offsets anyway)
        if(sourceSet == ResourceId())
          continue;

        const VulkanCreationInfo::PipelineLayout &pipeLayoutInfo =
            m_Creation.GetPipelineLayoutInfo(srcData.pipeLayout);

        ResourceId setOrig = sourceSet;

        const BindingStorage &bindStorage =
            m_pDriver->GetCurrentDescSetBindingStorage(srcData.descSet);
        const DescriptorSetSlot *first = bindStorage.binds.empty() ? NULL : bindStorage.binds[0];
        for(size_t b = 0; b < bindStorage.binds.size(); b++)
        {
          const DescSetLayout::Binding &layoutBind =
              m_Creation.GetDescSetLayout(pipeLayoutInfo.descSetLayouts[i]).bindings[b];

          if(layoutBind.layoutDescType != VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC &&
             layoutBind.layoutDescType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC)
            continue;

          uint64_t descriptorByteOffset = bindStorage.binds[b] - first;

          // inline UBOs aren't dynamic and variable size can't be used with dynamic buffers, so
          // the count is what it is at definition time
          for(uint32_t a = 0; a < layoutBind.descriptorCount; a++)
          {
            uint32_t dynamicBufferByteOffset = *srcOffset;
            srcOffset++;

            for(size_t accIdx = 0; accIdx < m_Access.size(); accIdx++)
            {
              if(m_Access[accIdx].descriptorStore == setOrig &&
                 m_Access[accIdx].byteOffset == descriptorByteOffset + a)
              {
                m_Descriptors[accIdx].byteOffset += dynamicBufferByteOffset;
                break;
              }
            }
          }
        }
      }
    }
    RDCASSERT(ShaderDebugData::MAX_QUEUED_OPS * mathOpResultByteSize <
              m_DebugData.ReadbackBuffer.TotalSize());
    RDCASSERT(sampleGatherOpResultsStart +
                  ShaderDebugData::MAX_QUEUED_OPS * sampleGatherOpResultByteSize <
              m_DebugData.ReadbackBuffer.TotalSize());
  }

  ~VulkanAPIWrapper()
  {
    CHECK_DEVICE_THREAD();
    m_pDriver->FlushQ();

    VkDevice dev = m_pDriver->GetDev();
    for(auto it = m_SampleViews.begin(); it != m_SampleViews.end(); it++)
      m_pDriver->vkDestroyImageView(dev, it->second, NULL);
    for(auto it = m_BiasSamplers.begin(); it != m_BiasSamplers.end(); it++)
      m_pDriver->vkDestroySampler(dev, it->second, NULL);
  }

  void ResetReplay()
  {
    CHECK_DEVICE_THREAD();
    if(!m_ResourcesDirty)
    {
      VkMarkerRegion region("ResetReplay");
      // replay the action to get back to 'normal' state for this event, and mark that we need to
      // replay back to pristine state next time we need to fetch data.
      m_pDriver->ReplayLog(0, m_EventID, eReplay_OnlyDraw);
    }
    m_ResourcesDirty = true;
  }

  virtual void AddDebugMessage(MessageCategory cat, MessageSeverity sev, MessageSource src,
                               rdcstr desc) override
  {
    CHECK_DEVICE_THREAD();
    m_pDriver->AddDebugMessage(cat, sev, src, desc);
  }

  virtual GraphicsAPI GetGraphicsAPI() override { return GraphicsAPI::Vulkan; }

  virtual bool SimulateThreaded() override { return Vulkan_Debug_EnableShaderDebugMT(); }

  virtual ResourceId GetShaderID() override { return m_ShaderID; }

  virtual uint64_t GetBufferLength(const ShaderBindIndex &bind) override
  {
    rdcspv::DeviceOpResult opResult;
    size_t length = 0;
    // BufferFunction guarantees the buffer cache readlock whilst the function is called
    bool succeeded = BufferFunction(
        bind, [&length](bytebuf *data) { length = data->size(); }, opResult);
    RDCASSERT(succeeded);
    RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Succeeded);
    return length;
  }

  virtual void ReadLocationValue(int32_t location, ShaderVariable &var) override
  {
    RDCERR("Invalid by-location read");
  }

  virtual void ReadBufferValue(const ShaderBindIndex &bind, uint64_t offset, uint64_t byteSize,
                               void *dst) override
  {
    rdcspv::DeviceOpResult opResult;
    // BufferFunction guarantees the buffer cache readlock whilst the function is called
    bool succeeded = BufferFunction(
        bind,
        [offset, byteSize, dst](bytebuf *data) {
          if(offset + byteSize <= data->size())
            memcpy(dst, data->data() + (size_t)offset, (size_t)byteSize);
        },
        opResult);
    RDCASSERT(succeeded);
    RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Succeeded);
  }

  virtual void WriteBufferValue(const ShaderBindIndex &bind, uint64_t offset, uint64_t byteSize,
                                const void *src) override
  {
    rdcspv::DeviceOpResult opResult;
    // BufferFunction guarantees the buffer cache readlock whilst the function is called
    bool succeeded = BufferFunction(
        bind,
        [offset, byteSize, src](bytebuf *data) {
          if(offset + byteSize <= data->size())
            memcpy(data->data() + (size_t)offset, src, (size_t)byteSize);
        },
        opResult);
    RDCASSERT(succeeded);
    RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Succeeded);
  }

  virtual void ReadAddress(uint64_t address, uint64_t byteSize, void *dst) override
  {
    rdcspv::DeviceOpResult opResult;
    // BufferFunction guarantees the buffer cache readlock whilst the function is called
    bool succeeded = BufferFunction(
        address,
        [byteSize, dst](bytebuf *data, size_t offset) {
          if(offset + byteSize <= data->size())
            memcpy(dst, data->data() + offset, (size_t)byteSize);
        },
        opResult);
    RDCASSERT(succeeded);
    RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Succeeded);
  }

  virtual void WriteAddress(uint64_t address, uint64_t byteSize, const void *src) override
  {
    rdcspv::DeviceOpResult opResult;
    // BufferFunction guarantees the buffer cache readlock whilst the function is called
    bool succeeded = BufferFunction(
        address,
        [byteSize, src](bytebuf *data, size_t offset) {
          if(offset + byteSize <= data->size())
            memcpy(data->data() + offset, src, (size_t)byteSize);
        },
        opResult);
    RDCASSERT(succeeded);
    RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Succeeded);
  }

  // Called from any thread
  // Caller guarantees that if the image data is not cached then we are on the device thread
  virtual rdcspv::DeviceOpResult ReadTexel(const ShaderBindIndex &imageBind,
                                           const ShaderVariable &coord, uint32_t sample,
                                           ShaderVariable &output) override
  {
    rdcspv::DeviceOpResult opResult;
    bool isCached = false;
    {
      SCOPED_READLOCK(imageCacheLock);
      isCached = GetImageDataFromCache(imageBind, opResult) != NULL;
      RDCASSERTNOTEQUAL(opResult, rdcspv::DeviceOpResult::NeedsDevice);
    }

    if(!isCached)
    {
      // Add image data to the cache : cache should not be locked by this thread
      PopulateImage(imageBind);
    }

    {
      SCOPED_READLOCK(imageCacheLock);
      ImageData *result = GetImageDataFromCache(imageBind, opResult);
      if(!result)
      {
        RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Failed);
        return rdcspv::DeviceOpResult::Failed;
      }

      ImageData &data = *result;
      if(data.width == 0)
        return rdcspv::DeviceOpResult::Failed;

      uint32_t coords[4];
      for(int i = 0; i < 4; i++)
        coords[i] = uintComp(coord, i);

      if(coords[0] >= data.width || coords[1] >= data.height || coords[2] >= data.depth)
      {
        if(!IsDeviceThread())
          return rdcspv::DeviceOpResult::NeedsDevice;

        CHECK_DEVICE_THREAD();
        m_pDriver->AddDebugMessage(
            MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
            StringFormat::Fmt(
                "Out of bounds access to image, coord %u,%u,%u outside of dimensions %ux%ux%u",
                coords[0], coords[1], coords[2], data.width, data.height, data.depth));
        return rdcspv::DeviceOpResult::Failed;
      }

      CompType varComp = VarTypeCompType(output.type);

      set0001(output);

      ShaderVariable input;
      input.columns = data.fmt.compCount;

      // the only 'irregular' format we need to worry about handling for integer types is
      // 10:10:10:2. All others are float/uint
      if(data.fmt.type == ResourceFormatType::R10G10B10A2)
      {
        PixelValue val;
        DecodePixelData(data.fmt, data.texel(coords, sample), val);

        if(data.fmt.compType == CompType::UInt)
          input.type = VarType::UInt;
        else if(data.fmt.compType == CompType::SInt)
          input.type = VarType::SInt;
        else
          input.type = VarType::Float;

        memcpy(input.value.u32v.data(), val.uintValue.data(), val.uintValue.byteSize());

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
        {
          if(data.fmt.compType == CompType::UInt)
            setUintComp(output, c, uintComp(input, c));
          else if(data.fmt.compType == CompType::SInt)
            setIntComp(output, c, intComp(input, c));
          else
            setFloatComp(output, c, input.value.f32v[c]);
        }
      }
      else if(data.fmt.compType == CompType::UInt)
      {
        RDCASSERT(varComp == CompType::UInt, varComp);

        // set up input type for proper expansion below
        if(data.fmt.compByteWidth == 1)
          input.type = VarType::UByte;
        else if(data.fmt.compByteWidth == 2)
          input.type = VarType::UShort;
        else if(data.fmt.compByteWidth == 4)
          input.type = VarType::UInt;
        else if(data.fmt.compByteWidth == 8)
          input.type = VarType::ULong;

        memcpy(input.value.u8v.data(), data.texel(coords, sample), data.texelSize);

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setUintComp(output, c, uintComp(input, c));
      }
      else if(data.fmt.compType == CompType::SInt)
      {
        RDCASSERT(varComp == CompType::SInt, varComp);

        // set up input type for proper expansion below
        if(data.fmt.compByteWidth == 1)
          input.type = VarType::SByte;
        else if(data.fmt.compByteWidth == 2)
          input.type = VarType::SShort;
        else if(data.fmt.compByteWidth == 4)
          input.type = VarType::SInt;
        else if(data.fmt.compByteWidth == 8)
          input.type = VarType::SLong;

        memcpy(input.value.u8v.data(), data.texel(coords, sample), data.texelSize);

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setIntComp(output, c, intComp(input, c));
      }
      else
      {
        RDCASSERT(varComp == CompType::Float, varComp);

        // do the decode of whatever unorm/float/etc the format is
        FloatVector v = DecodeFormattedComponents(data.fmt, data.texel(coords, sample));

        // set it into f32v
        input.value.f32v[0] = v.x;
        input.value.f32v[1] = v.y;
        input.value.f32v[2] = v.z;
        input.value.f32v[3] = v.w;

        // read as floats
        input.type = VarType::Float;

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setFloatComp(output, c, input.value.f32v[c]);
      }
    }

    return rdcspv::DeviceOpResult::Succeeded;
  }

  // Called from any thread
  // Caller guarantees that if the image data is not cached then we are on the device thread
  virtual rdcspv::DeviceOpResult WriteTexel(const ShaderBindIndex &imageBind,
                                            const ShaderVariable &coord, uint32_t sample,
                                            const ShaderVariable &input) override
  {
    rdcspv::DeviceOpResult opResult;
    ImageData *result = NULL;
    {
      SCOPED_READLOCK(imageCacheLock);
      result = GetImageDataFromCache(imageBind, opResult);
      RDCASSERTNOTEQUAL(opResult, rdcspv::DeviceOpResult::NeedsDevice);
    }

    if(!result)
    {
      // Add image data to the cache : cache should not be locked by this thread
      PopulateImage(imageBind);
    }

    {
      SCOPED_READLOCK(imageCacheLock);
      result = GetImageDataFromCache(imageBind, opResult);
      if(!result)
        return rdcspv::DeviceOpResult::Failed;

      ImageData &data = *result;
      if(data.width == 0)
        return rdcspv::DeviceOpResult::Failed;

      uint32_t coords[4];
      for(int i = 0; i < 4; i++)
        coords[i] = uintComp(coord, i);

      if(coords[0] >= data.width || coords[1] >= data.height || coords[2] >= data.depth)
      {
        if(!IsDeviceThread())
          return rdcspv::DeviceOpResult::NeedsDevice;

        CHECK_DEVICE_THREAD();
        m_pDriver->AddDebugMessage(
            MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
            StringFormat::Fmt(
                "Out of bounds access to image, coord %u,%u,%u outside of dimensions %ux%ux%u",
                coords[0], coords[1], coords[2], data.width, data.height, data.depth));
        return rdcspv::DeviceOpResult::Failed;
      }

      CompType varComp = VarTypeCompType(input.type);

      ShaderVariable output;
      output.columns = data.fmt.compCount;

      // the only 'irregular' format we need to worry about handling for integer types is
      // 10:10:10:2. All others are float/uint
      if(data.fmt.type == ResourceFormatType::R10G10B10A2)
      {
        // image writes are required to write a whole texel so we know we should have 4 components
        RDCASSERTEQUAL(input.columns, 4);

        uint32_t encoded = 0;

        if(data.fmt.compType == CompType::SNorm)
          encoded = ConvertToR10G10B10A2SNorm(Vec4f(input.value.f32v[0], input.value.f32v[1],
                                                    input.value.f32v[2], input.value.f32v[3]));
        else if(data.fmt.compType == CompType::UInt)
          encoded = ConvertToR10G10B10A2(Vec4u(input.value.u32v[0], input.value.u32v[1],
                                               input.value.u32v[2], input.value.u32v[3]));
        else
          encoded = ConvertToR10G10B10A2(Vec4f(input.value.f32v[0], input.value.f32v[1],
                                               input.value.f32v[2], input.value.f32v[3]));

        memcpy(data.texel(coords, sample), &encoded, sizeof(uint32_t));
      }
      else if(data.fmt.compType == CompType::UInt)
      {
        RDCASSERT(varComp == CompType::UInt, varComp);

        // set up output type for proper expansion below
        if(data.fmt.compByteWidth == 1)
          output.type = VarType::UByte;
        else if(data.fmt.compByteWidth == 2)
          output.type = VarType::UShort;
        else if(data.fmt.compByteWidth == 4)
          output.type = VarType::UInt;
        else if(data.fmt.compByteWidth == 8)
          output.type = VarType::ULong;

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setUintComp(output, c, uintComp(input, c));

        memcpy(data.texel(coords, sample), output.value.u8v.data(), data.texelSize);
      }
      else if(data.fmt.compType == CompType::SInt)
      {
        RDCASSERT(varComp == CompType::SInt, varComp);

        // set up input type for proper expansion below
        if(data.fmt.compByteWidth == 1)
          output.type = VarType::SByte;
        else if(data.fmt.compByteWidth == 2)
          output.type = VarType::SShort;
        else if(data.fmt.compByteWidth == 4)
          output.type = VarType::SInt;
        else if(data.fmt.compByteWidth == 8)
          output.type = VarType::SLong;

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setIntComp(output, c, intComp(input, c));

        memcpy(data.texel(coords, sample), output.value.u8v.data(), data.texelSize);
      }
      else
      {
        RDCASSERT(varComp == CompType::Float, varComp);

        // read as floats
        output.type = VarType::Float;

        for(uint8_t c = 0; c < RDCMIN(output.columns, input.columns); c++)
          setFloatComp(output, c, input.value.f32v[c]);

        FloatVector v;

        // set it into f32v
        v.x = input.value.f32v[0];
        v.y = input.value.f32v[1];
        v.z = input.value.f32v[2];
        v.w = input.value.f32v[3];

        EncodeFormattedComponents(data.fmt, v, data.texel(coords, sample));
      }
    }
    return rdcspv::DeviceOpResult::Succeeded;
  }

  // Can be called from any thread
  virtual void FillInputValue(ShaderVariable &var, ShaderBuiltin builtin, uint32_t threadIndex,
                              uint32_t location, uint32_t component) const override
  {
    if(!inputVarsReadOnly)
    {
      RDCERR("Input variables still being filled in");
      return;
    }
    if(builtin != ShaderBuiltin::Undefined)
    {
      if(threadIndex < thread_builtins.size())
      {
        auto it = thread_builtins[threadIndex].find(builtin);
        if(it != thread_builtins[threadIndex].end())
        {
          var.value = it->second.value;
          return;
        }
      }

      auto it = global_builtins.find(builtin);
      if(it != global_builtins.end())
      {
        var.value = it->second.value;
        return;
      }

      RDCERR("Couldn't get input for %s", ToStr(builtin).c_str());
      return;
    }

    if(threadIndex < location_inputs.size())
    {
      if(location < location_inputs[threadIndex].size())
      {
        if(var.rows == 1)
        {
          if(component + var.columns > 4)
            RDCERR("Unexpected component %u for column count %u", component, var.columns);

          for(uint8_t c = 0; c < var.columns; c++)
            copyComp(var, c, location_inputs[threadIndex][location], component + c);
        }
        else
        {
          RDCASSERTEQUAL(component, 0);
          for(uint8_t r = 0; r < var.rows; r++)
            for(uint8_t c = 0; c < var.columns; c++)
              copyComp(var, r * var.columns + c, location_inputs[threadIndex][location + c], r);
        }
        return;
      }
    }

    RDCERR("Couldn't get input for %s at thread=%u, location=%u, component=%u", var.name.c_str(),
           threadIndex, location, component);
  }

  uint32_t GetThreadProperty(uint32_t threadIndex, rdcspv::ThreadProperty prop) override
  {
    CHECK_DEVICE_THREAD();
    if(prop >= rdcspv::ThreadProperty::Count)
      return 0;
    if(threadIndex >= thread_props.size())
      return 0;

    return thread_props[threadIndex][(size_t)prop];
  }

  bool QueueSampleGather(rdcspv::ThreadState &lane, rdcspv::Op opcode,
                         DebugAPIWrapper::TextureType texType, const ShaderBindIndex &imageBind,
                         const ShaderBindIndex &samplerBind, const ShaderVariable &uv,
                         const ShaderVariable &ddxCalc, const ShaderVariable &ddyCalc,
                         const ShaderVariable &compare, rdcspv::GatherChannel gatherChannel,
                         const rdcspv::ImageOperandsAndParamDatas &operands, ShaderVariable &output,
                         bool &hasResult) override
  {
    CHECK_DEVICE_THREAD();
    ShaderConstParameters constParams = {};
    ShaderUniformParameters uniformParams = {};

    const bool buffer = (texType & DebugAPIWrapper::Buffer_Texture) != 0;
    const bool uintTex = (texType & DebugAPIWrapper::UInt_Texture) != 0;
    const bool sintTex = (texType & DebugAPIWrapper::SInt_Texture) != 0;

    // fetch the right type of descriptor depending on if we're buffer or not
    bool valid = true;
    rdcstr access = StringFormat::Fmt("performing %s operation", ToStr(opcode).c_str());
    const Descriptor &imageDescriptor = buffer ? GetDescriptor(access, ShaderBindIndex(), valid)
                                               : GetDescriptor(access, imageBind, valid);
    const Descriptor &bufferViewDescriptor = buffer
                                                 ? GetDescriptor(access, imageBind, valid)
                                                 : GetDescriptor(access, ShaderBindIndex(), valid);

    // fetch the sampler (if there's no sampler, this will silently return dummy data without
    // marking invalid
    const SamplerDescriptor &samplerDescriptor = GetSamplerDescriptor(access, samplerBind, valid);

    // if any descriptor lookup failed, return now
    if(!valid)
    {
      hasResult = false;
      return false;
    }

    VkMarkerRegion markerRegion("QueueSampleGather");

    VkBufferView bufferView =
        m_pDriver->GetResourceManager()->GetHandle<VkBufferView>(bufferViewDescriptor.view);

    VkSampler sampler =
        m_pDriver->GetResourceManager()->GetHandle<VkSampler>(samplerDescriptor.object);
    VkImageView view = m_pDriver->GetResourceManager()->GetHandle<VkImageView>(imageDescriptor.view);
    VkImageLayout layout = convert((DescriptorSlotImageLayout)imageDescriptor.byteOffset);

    // NULL view : return 0,0,0,0
    if(!buffer && (view == VK_NULL_HANDLE))
    {
      memset(&output.value, 0, sizeof(output.value));
      hasResult = true;
      return true;
    }

    // promote view to Array view
    VulkanCreationInfo::ImageView defaultViewProps = {};
    VulkanCreationInfo::Image defaultImageProps = {};
    VulkanCreationInfo::Buffer defaultBufferProps = {};
    VulkanCreationInfo::Sampler defaultSamplerProps = {};

    const VulkanCreationInfo::ImageView &viewProps =
        buffer ? defaultViewProps : m_Creation.GetImageViewInfo(GetResID(view));

    // NULL image : return 0,0,0,0
    if(!buffer && (viewProps.image == ResourceId()))
    {
      memset(&output.value, 0, sizeof(output.value));
      hasResult = true;
      return true;
    }

    const VulkanCreationInfo::Image &imageProps =
        buffer ? defaultImageProps : m_Creation.GetImageInfo(viewProps.image);

    const bool depthTex = IsDepthOrStencilFormat(viewProps.format);

    VkDevice dev = m_pDriver->GetDev();

    // how many co-ordinates should there be
    int coords = 0, gradCoords = 0;
    if(buffer)
    {
      constParams.dim = ShaderDebugBind::Buffer;
      coords = gradCoords = 1;
    }
    else
    {
      switch(viewProps.viewType)
      {
        case VK_IMAGE_VIEW_TYPE_1D:
          coords = 1;
          gradCoords = 1;
          constParams.dim = ShaderDebugBind::Tex1D;
          break;
        case VK_IMAGE_VIEW_TYPE_2D:
          coords = 2;
          gradCoords = 2;
          constParams.dim = ShaderDebugBind::Tex2D;
          break;
        case VK_IMAGE_VIEW_TYPE_3D:
          coords = 3;
          gradCoords = 3;
          constParams.dim = ShaderDebugBind::Tex3D;
          break;
        case VK_IMAGE_VIEW_TYPE_CUBE:
          coords = 3;
          gradCoords = 3;
          constParams.dim = ShaderDebugBind::TexCube;
          break;
        case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
          coords = 2;
          gradCoords = 1;
          constParams.dim = ShaderDebugBind::Tex1D;
          break;
        case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
          coords = 3;
          gradCoords = 2;
          constParams.dim = ShaderDebugBind::Tex2D;
          break;
        case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
          coords = 4;
          gradCoords = 3;
          constParams.dim = ShaderDebugBind::TexCube;
          break;
        case VK_IMAGE_VIEW_TYPE_MAX_ENUM:
          RDCERR("Invalid image view type %s", ToStr(viewProps.viewType).c_str());
          return false;
      }

      if(imageProps.samples > 1)
        constParams.dim = ShaderDebugBind::Tex2DMS;
    }

    // handle query opcodes now
    switch(opcode)
    {
      case rdcspv::Op::ImageQueryLevels:
      {
        output.value.u32v[0] = viewProps.range.levelCount;
        if(viewProps.range.levelCount == VK_REMAINING_MIP_LEVELS)
          output.value.u32v[0] = imageProps.mipLevels - viewProps.range.baseMipLevel;
        hasResult = true;
        return true;
      }
      case rdcspv::Op::ImageQuerySamples:
      {
        output.value.u32v[0] = (uint32_t)imageProps.samples;
        hasResult = true;
        return true;
      }
      case rdcspv::Op::ImageQuerySize:
      case rdcspv::Op::ImageQuerySizeLod:
      {
        uint32_t mip = viewProps.range.baseMipLevel;

        if(opcode == rdcspv::Op::ImageQuerySizeLod)
          mip += uintComp(lane.GetSrc(operands.lod), 0);

        RDCEraseEl(output.value);

        int i = 0;
        setUintComp(output, i++, RDCMAX(1U, imageProps.extent.width >> mip));
        if(coords >= 2)
          setUintComp(output, i++, RDCMAX(1U, imageProps.extent.height >> mip));
        if(viewProps.viewType == VK_IMAGE_VIEW_TYPE_3D)
          setUintComp(output, i++, RDCMAX(1U, imageProps.extent.depth >> mip));

        if(viewProps.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
           viewProps.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)
          setUintComp(output, i++, imageProps.arrayLayers);
        else if(viewProps.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
                viewProps.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY)
          setUintComp(output, i++, imageProps.arrayLayers / 6);

        if(buffer)
        {
          VkDeviceSize size;
          VkFormat format;
          if(bufferView == VK_NULL_HANDLE)
          {
            // descriptor buffer case - there is no buffer view so read directly out of the determined descriptor
            format = MakeVkFormat(bufferViewDescriptor.format);
            // size is not allowed to be VK_WHOLE_SIZE
            size = bufferViewDescriptor.byteSize;
          }
          else
          {
            const VulkanCreationInfo::BufferView &bufViewProps =
                m_Creation.GetBufferViewInfo(GetResID(bufferView));

            size = bufViewProps.size;
            format = bufViewProps.format;

            if(size == VK_WHOLE_SIZE)
            {
              const VulkanCreationInfo::Buffer &bufProps =
                  bufViewProps.buffer == ResourceId()
                      ? defaultBufferProps
                      : m_Creation.GetBufferInfo(bufViewProps.buffer);
              size = bufProps.size - bufViewProps.offset;
            }
          }

          setUintComp(output, 0, uint32_t(size / GetByteSize(1, 1, 1, format, 0)));
        }

        hasResult = true;
        return true;
      }
      default: break;
    }

    // create our own view (if we haven't already for this view) so we can promote to array
    VkImageView sampleView = m_SampleViews[GetResID(view)];
    if(sampleView == VK_NULL_HANDLE && view != VK_NULL_HANDLE)
    {
      VkImageViewCreateInfo viewInfo = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
      viewInfo.image = m_pDriver->GetResourceManager()->GetHandle<VkImage>(viewProps.image);
      viewInfo.format = viewProps.format;
      viewInfo.viewType = viewProps.viewType;
      if(viewInfo.viewType == VK_IMAGE_VIEW_TYPE_1D)
        viewInfo.viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY;
      else if(viewInfo.viewType == VK_IMAGE_VIEW_TYPE_2D)
        viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
      else if(viewInfo.viewType == VK_IMAGE_VIEW_TYPE_CUBE &&
              m_pDriver->GetDeviceEnabledFeatures().imageCubeArray)
        viewInfo.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;

      viewInfo.components = viewProps.componentMapping;
      viewInfo.subresourceRange = viewProps.range;

      // if KHR_maintenance2 is available, ensure we have sampled usage available
      VkImageViewUsageCreateInfo usageCreateInfo = {VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
      if(m_pDriver->GetExtensions(NULL).ext_KHR_maintenance2)
      {
        usageCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
        viewInfo.pNext = &usageCreateInfo;
      }

      VkResult vkr = m_pDriver->vkCreateImageView(dev, &viewInfo, NULL, &sampleView);
      CHECK_VKR(m_pDriver, vkr);

      m_SampleViews[GetResID(view)] = sampleView;
    }

    if(operands.flags & rdcspv::ImageOperands::Bias)
    {
      const ShaderVariable &biasVar = lane.GetSrc(operands.bias);

      // silently cast parameters to 32-bit floats
      float bias = floatComp(biasVar, 0);

      if(bias != 0.0f)
      {
        // bias can only be used with implicit lod operations, but we want to do everything with
        // explicit lod operations. So we instead push the bias into a new sampler, which is
        // entirely equivalent.

        // first check to see if we have one already, since the bias is probably going to be
        // coherent.
        SamplerBiasKey key = {GetResID(sampler), bias};

        auto insertIt = m_BiasSamplers.insert(std::make_pair(key, VkSampler()));
        if(insertIt.second)
        {
          const VulkanCreationInfo::Sampler &samplerProps =
              (sampler == VK_NULL_HANDLE) ? defaultSamplerProps
                                          : m_Creation.GetSamplerInfo(key.first);

          VkSamplerCreateInfo sampInfo = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
          sampInfo.magFilter = samplerProps.magFilter;
          sampInfo.minFilter = samplerProps.minFilter;
          sampInfo.mipmapMode = samplerProps.mipmapMode;
          sampInfo.addressModeU = samplerProps.address[0];
          sampInfo.addressModeV = samplerProps.address[1];
          sampInfo.addressModeW = samplerProps.address[2];
          sampInfo.mipLodBias = samplerProps.mipLodBias;
          sampInfo.anisotropyEnable = samplerProps.maxAnisotropy >= 1.0f;
          sampInfo.maxAnisotropy = samplerProps.maxAnisotropy;
          sampInfo.compareEnable = samplerProps.compareEnable;
          sampInfo.compareOp = samplerProps.compareOp;
          sampInfo.minLod = samplerProps.minLod;
          sampInfo.maxLod = samplerProps.maxLod;
          sampInfo.borderColor = samplerProps.borderColor;
          sampInfo.unnormalizedCoordinates = samplerProps.unnormalizedCoordinates;

          VkSamplerReductionModeCreateInfo reductionInfo = {
              VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO};
          if(samplerProps.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE)
          {
            reductionInfo.reductionMode = samplerProps.reductionMode;

            reductionInfo.pNext = sampInfo.pNext;
            sampInfo.pNext = &reductionInfo;
          }

          VkSamplerYcbcrConversionInfo ycbcrInfo = {VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
          if(samplerProps.ycbcr != ResourceId())
          {
            ycbcrInfo.conversion =
                m_pDriver->GetResourceManager()->GetHandle<VkSamplerYcbcrConversion>(viewProps.image);

            ycbcrInfo.pNext = sampInfo.pNext;
            sampInfo.pNext = &ycbcrInfo;
          }

          VkSamplerCustomBorderColorCreateInfoEXT borderInfo = {
              VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT};
          if(samplerProps.customBorder)
          {
            borderInfo.customBorderColor = samplerProps.customBorderColor;
            borderInfo.format = samplerProps.customBorderFormat;

            borderInfo.pNext = sampInfo.pNext;
            sampInfo.pNext = &borderInfo;
          }

          // now add the shader's bias on
          sampInfo.mipLodBias += bias;

          VkResult vkr = m_pDriver->vkCreateSampler(dev, &sampInfo, NULL, &sampler);
          CHECK_VKR(m_pDriver, vkr);

          insertIt.first->second = sampler;
        }
        else
        {
          sampler = insertIt.first->second;
        }
      }
    }

    constParams.operation = (uint32_t)opcode;

    // proj opcodes have an extra q parameter, but we do the divide ourselves and 'demote' these to
    // non-proj variants
    bool proj = false;
    switch(opcode)
    {
      case rdcspv::Op::ImageSampleProjExplicitLod:
      {
        constParams.operation = (uint32_t)rdcspv::Op::ImageSampleExplicitLod;
        proj = true;
        break;
      }
      case rdcspv::Op::ImageSampleProjImplicitLod:
      {
        constParams.operation = (uint32_t)rdcspv::Op::ImageSampleImplicitLod;
        proj = true;
        break;
      }
      case rdcspv::Op::ImageSampleProjDrefExplicitLod:
      {
        constParams.operation = (uint32_t)rdcspv::Op::ImageSampleDrefExplicitLod;
        proj = true;
        break;
      }
      case rdcspv::Op::ImageSampleProjDrefImplicitLod:
      {
        constParams.operation = (uint32_t)rdcspv::Op::ImageSampleDrefImplicitLod;
        proj = true;
        break;
      }
      default: break;
    }

    bool useCompare = false;
    switch(opcode)
    {
      case rdcspv::Op::ImageDrefGather:
      case rdcspv::Op::ImageSampleDrefExplicitLod:
      case rdcspv::Op::ImageSampleDrefImplicitLod:
      case rdcspv::Op::ImageSampleProjDrefExplicitLod:
      case rdcspv::Op::ImageSampleProjDrefImplicitLod:
      {
        useCompare = true;

        if(m_pDriver->GetDriverInfo().QualcommDrefNon2DCompileCrash() &&
           constParams.dim != ShaderDebugBind::Tex2D)
        {
          m_pDriver->AddDebugMessage(
              MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
              "Dref sample against non-2D texture, this cannot be debugged due to a driver bug");
        }

        break;
      }
      default: break;
    }

    bool gatherOp = false;

    switch(opcode)
    {
      case rdcspv::Op::ImageFetch:
      {
        // co-ordinates after the used ones are read as 0s. This allows us to then read an implicit
        // 0 for array layer when we promote accesses to arrays.
        uniformParams.texel_uvw.x = uintComp(uv, 0);
        if(coords >= 2)
          uniformParams.texel_uvw.y = uintComp(uv, 1);
        if(coords >= 3)
          uniformParams.texel_uvw.z = uintComp(uv, 2);

        if(!buffer && operands.flags & rdcspv::ImageOperands::Lod)
          uniformParams.texel_lod = uintComp(lane.GetSrc(operands.lod), 0);
        else
          uniformParams.texel_lod = 0;

        if(operands.flags & rdcspv::ImageOperands::Sample)
          uniformParams.sampleIdx = uintComp(lane.GetSrc(operands.sample), 0);

        break;
      }
      case rdcspv::Op::ImageGather:
      case rdcspv::Op::ImageDrefGather:
      {
        gatherOp = true;

        // silently cast parameters to 32-bit floats
        for(int i = 0; i < coords; i++)
          uniformParams.uvwa[i] = floatComp(uv, i);

        if(useCompare)
          uniformParams.compare = floatComp(compare, 0);

        constParams.gatherChannel = gatherChannel;

        if(operands.flags & rdcspv::ImageOperands::ConstOffsets)
        {
          ShaderVariable constOffsets = lane.GetSrc(operands.constOffsets);

          constParams.useGradOrGatherOffsets = VK_TRUE;

          // should be an array of ivec2
          RDCASSERT(constOffsets.members.size() == 4);

          // sign extend variables lower than 32-bits
          for(int i = 0; i < 4; i++)
          {
            if(constOffsets.members[i].type == VarType::SByte)
            {
              constOffsets.members[i].value.s32v[0] = constOffsets.members[i].value.s8v[0];
              constOffsets.members[i].value.s32v[1] = constOffsets.members[i].value.s8v[1];
            }
            else if(constOffsets.members[i].type == VarType::SShort)
            {
              constOffsets.members[i].value.s32v[0] = constOffsets.members[i].value.s16v[0];
              constOffsets.members[i].value.s32v[1] = constOffsets.members[i].value.s16v[1];
            }
          }

          constParams.gatherOffsets.u0 = constOffsets.members[0].value.s32v[0];
          constParams.gatherOffsets.v0 = constOffsets.members[0].value.s32v[1];
          constParams.gatherOffsets.u1 = constOffsets.members[1].value.s32v[0];
          constParams.gatherOffsets.v1 = constOffsets.members[1].value.s32v[1];
          constParams.gatherOffsets.u2 = constOffsets.members[2].value.s32v[0];
          constParams.gatherOffsets.v2 = constOffsets.members[2].value.s32v[1];
          constParams.gatherOffsets.u3 = constOffsets.members[3].value.s32v[0];
          constParams.gatherOffsets.v3 = constOffsets.members[3].value.s32v[1];
        }

        break;
      }
      case rdcspv::Op::ImageQueryLod:
      case rdcspv::Op::ImageSampleExplicitLod:
      case rdcspv::Op::ImageSampleImplicitLod:
      case rdcspv::Op::ImageSampleProjExplicitLod:
      case rdcspv::Op::ImageSampleProjImplicitLod:
      case rdcspv::Op::ImageSampleDrefExplicitLod:
      case rdcspv::Op::ImageSampleDrefImplicitLod:
      case rdcspv::Op::ImageSampleProjDrefExplicitLod:
      case rdcspv::Op::ImageSampleProjDrefImplicitLod:
      {
        // silently cast parameters to 32-bit floats
        for(int i = 0; i < coords; i++)
          uniformParams.uvwa[i] = floatComp(uv, i);

        if(proj)
        {
          // coords shouldn't be 4 because that's only valid for cube arrays which can't be
          // projected
          RDCASSERT(coords < 4);

          // do the divide ourselves rather than severely complicating the sample shader (as proj
          // variants need non-arrayed textures)
          float q = floatComp(uv, coords);

          uniformParams.uvwa[0] /= q;
          uniformParams.uvwa[1] /= q;
          uniformParams.uvwa[2] /= q;
        }

        if(operands.flags & rdcspv::ImageOperands::MinLod)
        {
          const ShaderVariable &minLodVar = lane.GetSrc(operands.minLod);

          // silently cast parameters to 32-bit floats
          uniformParams.minlod = floatComp(minLodVar, 0);
        }

        if(useCompare)
        {
          // silently cast parameters to 32-bit floats
          uniformParams.compare = floatComp(compare, 0);
        }

        if(operands.flags & rdcspv::ImageOperands::Lod)
        {
          const ShaderVariable &lodVar = lane.GetSrc(operands.lod);

          // silently cast parameters to 32-bit floats
          uniformParams.lod = floatComp(lodVar, 0);
          constParams.useGradOrGatherOffsets = VK_FALSE;
        }
        else if(operands.flags & rdcspv::ImageOperands::Grad)
        {
          ShaderVariable ddx = lane.GetSrc(operands.grad.first);
          ShaderVariable ddy = lane.GetSrc(operands.grad.second);

          constParams.useGradOrGatherOffsets = VK_TRUE;

          // silently cast parameters to 32-bit floats
          RDCASSERTEQUAL(ddx.type, ddy.type);
          for(int i = 0; i < gradCoords; i++)
          {
            uniformParams.ddx[i] = floatComp(ddx, i);
            uniformParams.ddy[i] = floatComp(ddy, i);
          }
        }

        if(opcode == rdcspv::Op::ImageSampleImplicitLod ||
           opcode == rdcspv::Op::ImageSampleProjImplicitLod || opcode == rdcspv::Op::ImageQueryLod)
        {
          // use grad to sub in for the implicit lod
          constParams.useGradOrGatherOffsets = VK_TRUE;

          // silently cast parameters to 32-bit floats
          RDCASSERTEQUAL(ddxCalc.type, ddyCalc.type);
          for(int i = 0; i < gradCoords; i++)
          {
            uniformParams.ddx[i] = floatComp(ddxCalc, i);
            uniformParams.ddy[i] = floatComp(ddyCalc, i);
          }
        }
        if((opcode == rdcspv::Op::ImageSampleProjDrefExplicitLod) ||
           (opcode == rdcspv::Op::ImageSampleProjDrefImplicitLod))
        {
          RDCASSERT(useCompare);
          float q = floatComp(uv, coords);
          uniformParams.compare /= q;
        }

        break;
      }
      default:
      {
        RDCERR("Unsupported opcode %s", ToStr(opcode).c_str());
        return false;
      }
    }

    if(operands.flags & rdcspv::ImageOperands::ConstOffset)
    {
      ShaderVariable constOffset = lane.GetSrc(operands.constOffset);

      // sign extend variables lower than 32-bits
      for(uint8_t c = 0; c < constOffset.columns; c++)
      {
        if(constOffset.type == VarType::SByte)
          constOffset.value.s32v[c] = constOffset.value.s8v[c];
        else if(constOffset.type == VarType::SShort)
          constOffset.value.s32v[c] = constOffset.value.s16v[c];
      }

      // pass offsets as uniform where possible - when the feature (widely available) on gather
      // operations. On non-gather operations we are forced to use const offsets and must specialise
      // the pipeline.
      if(m_pDriver->GetDeviceEnabledFeatures().shaderImageGatherExtended && gatherOp)
      {
        uniformParams.offset.x = constOffset.value.s32v[0];
        if(gradCoords >= 2)
          uniformParams.offset.y = constOffset.value.s32v[1];
        if(gradCoords >= 3)
          uniformParams.offset.z = constOffset.value.s32v[2];
      }
      else
      {
        constParams.constOffsets.x = constOffset.value.s32v[0];
        if(gradCoords >= 2)
          constParams.constOffsets.y = constOffset.value.s32v[1];
        if(gradCoords >= 3)
          constParams.constOffsets.z = constOffset.value.s32v[2];
      }
    }
    else if(operands.flags & rdcspv::ImageOperands::Offset)
    {
      ShaderVariable offset = lane.GetSrc(operands.offset);

      // sign extend variables lower than 32-bits
      for(uint8_t c = 0; c < offset.columns; c++)
      {
        if(offset.type == VarType::SByte)
          offset.value.s32v[c] = offset.value.s8v[c];
        else if(offset.type == VarType::SShort)
          offset.value.s32v[c] = offset.value.s16v[c];
      }

      // if the app's shader used a dynamic offset, we can too!
      uniformParams.offset.x = offset.value.s32v[0];
      if(gradCoords >= 2)
        uniformParams.offset.y = offset.value.s32v[1];
      if(gradCoords >= 3)
        uniformParams.offset.z = offset.value.s32v[2];
    }

    if(!m_pDriver->GetDeviceEnabledFeatures().shaderImageGatherExtended &&
       (uniformParams.offset.x != 0 || uniformParams.offset.y != 0 || uniformParams.offset.z != 0))
    {
      m_pDriver->AddDebugMessage(
          MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
          StringFormat::Fmt("Use of constant offsets %d/%d/%d is not supported without "
                            "shaderImageGatherExtended device feature",
                            uniformParams.offset.x, uniformParams.offset.y, uniformParams.offset.z));
    }

    VkPipeline pipe = MakePipe(constParams, 32, depthTex, uintTex, sintTex);

    if(pipe == VK_NULL_HANDLE)
    {
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning,
                                 "Failed to compile graphics pipeline for sampling operation");
      return false;
    }

    VkCommandBuffer cmd = queuedOpCmdBuffer;
    if(cmd == VK_NULL_HANDLE)
    {
      if(StartQueuedOps())
        cmd = queuedOpCmdBuffer;
    }
    if(cmd == VK_NULL_HANDLE)
      return false;

    if(queueIndex >= ShaderDebugData::MAX_QUEUED_OPS)
    {
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning, "Too many GPU queued operations");
      return false;
    }

    VkDescriptorImageInfo samplerWriteInfo = {Unwrap(sampler), VK_NULL_HANDLE,
                                              VK_IMAGE_LAYOUT_UNDEFINED};
    VkDescriptorImageInfo imageWriteInfo = {VK_NULL_HANDLE, Unwrap(sampleView), layout};

    VkDescriptorBufferInfo uniformWriteInfo = {};
    m_DebugData.ConstantsBuffer.FillDescriptor(uniformWriteInfo);
    const VkDescriptorSet realDescSet = Unwrap(m_DebugData.DescSets[queueIndex++]);

    VkWriteDescriptorSet writeSets[] = {
        {
            VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
            NULL,
            realDescSet,
            (uint32_t)ShaderDebugBind::Constants,
            0,
            1,
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
            NULL,
            &uniformWriteInfo,
            NULL,
        },
        {
            VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
            NULL,
            realDescSet,
            (uint32_t)constParams.dim,
            0,
            1,
            VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
            &imageWriteInfo,
            NULL,
            NULL,
        },
        {
            VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
            NULL,
            realDescSet,
            (uint32_t)ShaderDebugBind::Sampler,
            0,
            1,
            VK_DESCRIPTOR_TYPE_SAMPLER,
            &samplerWriteInfo,
            NULL,
            NULL,
        },
    };

    if(buffer)
    {
      if(bufferView == VK_NULL_HANDLE)
      {
        // descriptor buffer, must create our own

        BufViewKey key = {bufferViewDescriptor.resource, bufferViewDescriptor.byteOffset,
                          bufferViewDescriptor.byteSize, MakeVkFormat(bufferViewDescriptor.format)};

        bufferView = m_SampleBufViews[key];
        if(bufferView == VK_NULL_HANDLE)
        {
          VkBufferViewCreateInfo viewInfo = {
              VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO,
              NULL,
              0,
              m_pDriver->GetResourceManager()->GetHandle<VkBuffer>(bufferViewDescriptor.resource),
              key.format,
              bufferViewDescriptor.byteOffset,
              bufferViewDescriptor.byteSize,
          };

          VkResult vkr = m_pDriver->vkCreateBufferView(dev, &viewInfo, NULL, &bufferView);
          CHECK_VKR(m_pDriver, vkr);

          m_SampleBufViews[key] = bufferView;
        }
      }

      writeSets[1].pTexelBufferView = UnwrapPtr(bufferView);
      writeSets[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
    }

    // reset descriptor sets to dummy state
    if(depthTex)
    {
      uint32_t resetIndex = 3;

      rdcarray<VkWriteDescriptorSet> writes;

      for(size_t i = 0; i < ARRAY_COUNT(m_DebugData.DummyWrites[resetIndex]); i++)
      {
        // not all textures may be supported for depth, so only update those that are valid
        if(m_DebugData.DummyWrites[resetIndex][i].descriptorCount != 0)
        {
          m_DebugData.DummyWrites[resetIndex][i].dstSet = realDescSet;
          writes.push_back(m_DebugData.DummyWrites[resetIndex][i]);
        }
      }

      ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), (uint32_t)writes.count(), writes.data(), 0,
                                         NULL);
    }
    else
    {
      uint32_t resetIndex = 0;
      if(uintTex)
        resetIndex = 1;
      else if(sintTex)
        resetIndex = 2;

      for(size_t i = 0; i < ARRAY_COUNT(m_DebugData.DummyWrites[resetIndex]); i++)
      {
        if(m_DebugData.DummyWrites[resetIndex][i].descriptorCount != 0)
          m_DebugData.DummyWrites[resetIndex][i].dstSet = realDescSet;
      }
      ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev),
                                         ARRAY_COUNT(m_DebugData.DummyWrites[resetIndex]),
                                         m_DebugData.DummyWrites[resetIndex], 0, NULL);
    }

    // overwrite with our data
    ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), sampler != VK_NULL_HANDLE ? 3 : 2, writeSets, 0,
                                       NULL);

    uint32_t constsOffset = 0;
    void *constants = m_DebugData.ConstantsBuffer.Map(&constsOffset, 0);
    if(!constants)
      return false;

    memcpy(constants, &uniformParams, sizeof(uniformParams));

    m_DebugData.ConstantsBuffer.Unmap();

    VkClearValue clear = {};

    VkRenderPassBeginInfo rpbegin = {
        VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
        NULL,
        Unwrap(m_DebugData.RenderPass),
        Unwrap(m_DebugData.Framebuffer),
        {{0, 0}, {1, 1}},
        1,
        &clear,
    };
    ObjDisp(cmd)->CmdBeginRenderPass(Unwrap(cmd), &rpbegin, VK_SUBPASS_CONTENTS_INLINE);

    ObjDisp(cmd)->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS, Unwrap(pipe));
    ObjDisp(cmd)->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                        Unwrap(m_DebugData.PipeLayout), 0, 1, &realDescSet, 1,
                                        &constsOffset);

    // push uvw/ddx/ddy for the vertex shader
    ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DebugData.PipeLayout), VK_SHADER_STAGE_ALL,
                                   sizeof(Vec4f) * 0, sizeof(Vec4f), &uniformParams.uvwa);
    ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DebugData.PipeLayout), VK_SHADER_STAGE_ALL,
                                   sizeof(Vec4f) * 1, sizeof(Vec3f), &uniformParams.ddx);
    ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DebugData.PipeLayout), VK_SHADER_STAGE_ALL,
                                   sizeof(Vec4f) * 2, sizeof(Vec3f), &uniformParams.ddy);

    ObjDisp(cmd)->CmdDraw(Unwrap(cmd), 3, 1, 0, 0);

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

    RDCASSERT(sampleGatherOpResultOffset + sampleGatherOpResultByteSize <=
                  m_DebugData.ReadbackBuffer.TotalSize(),
              sampleGatherOpResultOffset, sampleGatherOpResultByteSize,
              m_DebugData.ReadbackBuffer.TotalSize());
    VkBufferImageCopy region = {
        sampleGatherOpResultOffset,           sizeof(Vec4f), 1,
        {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, {0, 0, 0},     {1, 1, 1},
    };
    sampleGatherOpResultOffset += sampleGatherOpResultByteSize;
    ObjDisp(cmd)->CmdCopyImageToBuffer(Unwrap(cmd), Unwrap(m_DebugData.Image),
                                       VK_IMAGE_LAYOUT_GENERAL,
                                       m_DebugData.ReadbackBuffer.UnwrappedBuffer(), 1, &region);

    hasResult = false;
    return true;
  }

  virtual bool QueueCalculateMathOp(rdcspv::GLSLstd450 op,
                                    const rdcarray<ShaderVariable> &params) override
  {
    CHECK_DEVICE_THREAD();
    RDCASSERT(params.size() <= 3, params.size());

    int floatSizeIdx = 0;
    if(params[0].type == VarType::Half)
      floatSizeIdx = 1;
    else if(params[0].type == VarType::Double)
      floatSizeIdx = 2;

    if(m_DebugData.MathPipe[floatSizeIdx] == VK_NULL_HANDLE)
    {
      ShaderConstParameters pipeParams = {};
      pipeParams.operation = (uint32_t)rdcspv::Op::ExtInst;
      m_DebugData.MathPipe[floatSizeIdx] =
          MakePipe(pipeParams, VarTypeByteSize(params[0].type) * 8, false, false, false);

      if(m_DebugData.MathPipe[floatSizeIdx] == VK_NULL_HANDLE)
      {
        m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                   MessageSource::RuntimeWarning,
                                   "Failed to compile graphics pipeline for math operation");
        return false;
      }
    }

    VkCommandBuffer cmd = queuedOpCmdBuffer;
    if(cmd == VK_NULL_HANDLE)
    {
      if(StartQueuedOps())
        cmd = queuedOpCmdBuffer;
    }
    if(cmd == VK_NULL_HANDLE)
      return false;

    if(queueIndex >= ShaderDebugData::MAX_QUEUED_OPS)
    {
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning, "Too many GPU queued operations");
      return false;
    }
    VkDescriptorBufferInfo storageWriteInfo = {};
    m_DebugData.MathResult.FillDescriptor(storageWriteInfo);

    const VkDescriptorSet realDescSet = Unwrap(m_DebugData.DescSets[queueIndex++]);

    VkWriteDescriptorSet writeSets[] = {
        {
            VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
            NULL,
            realDescSet,
            (uint32_t)ShaderDebugBind::MathResult,
            0,
            1,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            NULL,
            &storageWriteInfo,
            NULL,
        },
    };

    VkDevice dev = m_pDriver->GetDev();

    ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev), 1, writeSets, 0, NULL);

    VkMarkerRegion markerRegion("QueueCalculateMathOp");

    ObjDisp(cmd)->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_COMPUTE,
                                  Unwrap(m_DebugData.MathPipe[floatSizeIdx]));

    uint32_t constsOffset = 0;
    ObjDisp(cmd)->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_COMPUTE,
                                        Unwrap(m_DebugData.PipeLayout), 0, 1, &realDescSet, 1,
                                        &constsOffset);

    // push the parameters
    for(size_t i = 0; i < params.size(); i++)
    {
      RDCASSERTEQUAL(params[i].type, params[0].type);
      double p[4] = {};
      memcpy(p, params[i].value.f32v.data(), VarTypeByteSize(params[i].type) * params[i].columns);
      ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DebugData.PipeLayout),
                                     VK_SHADER_STAGE_ALL, uint32_t(sizeof(p) * i), sizeof(p), p);
    }

    // push the operation afterwards
    ObjDisp(cmd)->CmdPushConstants(Unwrap(cmd), Unwrap(m_DebugData.PipeLayout), VK_SHADER_STAGE_ALL,
                                   sizeof(Vec4f) * 6, sizeof(uint32_t), &op);

    ObjDisp(cmd)->CmdDispatch(Unwrap(cmd), 1, 1, 1);

    VkBufferMemoryBarrier bufBarrier = {
        VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
        NULL,
        VK_ACCESS_SHADER_WRITE_BIT,
        VK_ACCESS_TRANSFER_READ_BIT,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        m_DebugData.MathResult.UnwrappedBuffer(),
        0,
        VK_WHOLE_SIZE,
    };

    DoPipelineBarrier(cmd, 1, &bufBarrier);

    RDCASSERT(mathOpResultOffset + mathOpResultByteSize <= m_DebugData.ReadbackBuffer.TotalSize(),
              mathOpResultOffset, mathOpResultByteSize, m_DebugData.ReadbackBuffer.TotalSize());
    VkBufferCopy bufCopy = {0, mathOpResultOffset, mathOpResultByteSize};
    mathOpResultOffset += mathOpResultByteSize;
    ObjDisp(cmd)->CmdCopyBuffer(Unwrap(cmd), m_DebugData.MathResult.UnwrappedBuffer(),
                                m_DebugData.ReadbackBuffer.UnwrappedBuffer(), 1, &bufCopy);
    return true;
  }

  bool StartQueuedOps()
  {
    CHECK_DEVICE_THREAD();

    RDCASSERTEQUAL(queueIndex, 0);
    RDCASSERTEQUAL(queuedOpCmdBuffer, VK_NULL_HANDLE);
    RDCASSERTEQUAL(mathOpResultOffset, 0);
    RDCASSERTEQUAL(sampleGatherOpResultOffset, 0);

    if(queuedOpCmdBuffer != VK_NULL_HANDLE)
      return false;

    queuedOpCmdBuffer = m_pDriver->GetNextCmd();
    if(queuedOpCmdBuffer == VK_NULL_HANDLE)
      return false;

    VkCommandBuffer cmd = queuedOpCmdBuffer;
    VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                          VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

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

    sampleGatherOpResultOffset = sampleGatherOpResultsStart;
    return true;
  }

  virtual bool GetQueuedResults(rdcarray<ShaderVariable *> &mathOpResults,
                                rdcarray<ShaderVariable *> &sampleGatherResults) override
  {
    CHECK_DEVICE_THREAD();
    if(queuedOpCmdBuffer == VK_NULL_HANDLE)
      return false;

    VkBufferMemoryBarrier bufBarrier = {
        VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
        NULL,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        VK_ACCESS_HOST_READ_BIT,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        m_DebugData.ReadbackBuffer.UnwrappedBuffer(),
        0,
        VK_WHOLE_SIZE,
    };

    VkCommandBuffer cmd = queuedOpCmdBuffer;
    if(cmd == VK_NULL_HANDLE)
      return false;

    // wait for copy to finish before reading back to host
    DoPipelineBarrier(cmd, 1, &bufBarrier);

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

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

    queueIndex = 0;
    queuedOpCmdBuffer = VK_NULL_HANDLE;
    mathOpResultOffset = 0;
    sampleGatherOpResultOffset = 0;

    byte *gpuResults = (byte *)m_DebugData.ReadbackBuffer.Map(NULL, 0);
    if(!gpuResults)
      return false;

    uintptr_t bufferEnd = (uintptr_t)(gpuResults + m_DebugData.ReadbackBuffer.TotalSize());

    byte *gpuMathOpResults = gpuResults;
    for(ShaderVariable *result : mathOpResults)
    {
      size_t countBytes = VarTypeByteSize(result->type) * result->columns;
      RDCASSERT((uintptr_t)gpuMathOpResults + countBytes <= bufferEnd, (uintptr_t)gpuMathOpResults,
                countBytes, bufferEnd);
      RDCASSERT(countBytes <= mathOpResultByteSize, countBytes, mathOpResultByteSize);
      memcpy(result->value.u32v.data(), gpuMathOpResults, countBytes);
      gpuMathOpResults += mathOpResultByteSize;
    }

    byte *gpuSampleGatherOpResults = gpuResults + sampleGatherOpResultsStart;
    for(ShaderVariable *result : sampleGatherResults)
    {
      float *retf = (float *)gpuSampleGatherOpResults;
      uint32_t *retu = (uint32_t *)gpuSampleGatherOpResults;
      int32_t *reti = (int32_t *)gpuSampleGatherOpResults;

      size_t countBytes = 16;
      RDCASSERT((uintptr_t)gpuSampleGatherOpResults + countBytes <= bufferEnd,
                (uintptr_t)gpuSampleGatherOpResults, countBytes, bufferEnd);
      RDCASSERT(countBytes <= sampleGatherOpResultByteSize, countBytes, sampleGatherOpResultByteSize);
      // convert full precision results, we did all sampling at 32-bit precision
      ShaderVariable &output = *result;
      for(uint8_t c = 0; c < 4; c++)
      {
        if(VarTypeCompType(output.type) == CompType::Float)
          setFloatComp(output, c, retf[c]);
        else if(VarTypeCompType(output.type) == CompType::SInt)
          setIntComp(output, c, reti[c]);
        else
          setUintComp(output, c, retu[c]);
      }
      gpuSampleGatherOpResults += sampleGatherOpResultByteSize;
    }

    m_DebugData.ReadbackBuffer.Unmap();
    return true;
  }

  virtual bool QueuedOpsHasSpace() override { return queueIndex < ShaderDebugData::MAX_QUEUED_OPS; }

  // Device thread only for mutable state
  std::unordered_map<ShaderBuiltin, ShaderVariable> &GetGlobalBuiltins()
  {
    CHECK_DEVICE_THREAD();
    if(inputVarsReadOnly)
      RDCERR("Input variables can't be modified");
    return global_builtins;
  }

  // Device thread only for mutable state
  rdcarray<std::unordered_map<ShaderBuiltin, ShaderVariable>> &GetThreadBuiltins()
  {
    CHECK_DEVICE_THREAD();
    if(inputVarsReadOnly)
      RDCERR("Input variables can't be modified");
    return thread_builtins;
  }

  // Device thread only for mutable state
  rdcarray<rdcarray<ShaderVariable>> &GetLocationInputs()
  {
    CHECK_DEVICE_THREAD();
    if(inputVarsReadOnly)
      RDCERR("Input variables can't be modified");
    return location_inputs;
  }

  // Device thread only for mutable state
  void SetInputVarsToReadOnly()
  {
    CHECK_DEVICE_THREAD();
    inputVarsReadOnly = true;
  }

  rdcarray<rdcfixedarray<uint32_t, arraydim<rdcspv::ThreadProperty>()>> thread_props;

  uint64_t GetDeviceThreadID() const { return deviceThreadID; }
  bool IsDeviceThread() const { return Threading::GetCurrentID() == GetDeviceThreadID(); }

private:
  WrappedVulkan *m_pDriver = NULL;
  ShaderDebugData &m_DebugData;
  const VulkanCreationInfo &m_Creation;

  bool m_ResourcesDirty = false;
  uint32_t m_EventID;
  ResourceId m_ShaderID;

  bool inputVarsReadOnly = false;
  // global over all threads
  std::unordered_map<ShaderBuiltin, ShaderVariable> global_builtins;
  // per-thread builtins
  rdcarray<std::unordered_map<ShaderBuiltin, ShaderVariable>> thread_builtins;
  // per-thread custom inputs by location [thread][location]
  rdcarray<rdcarray<ShaderVariable>> location_inputs;

  rdcarray<DescriptorAccess> m_Access;
  rdcarray<Descriptor> m_Descriptors;
  rdcarray<SamplerDescriptor> m_SamplerDescriptors;

  std::map<ResourceId, VkImageView> m_SampleViews;
  struct BufViewKey
  {
    ResourceId buf;
    VkDeviceSize offs, size;
    VkFormat format;
    bool operator==(const BufViewKey &o) const
    {
      return buf == o.buf && offs == o.offs && size == o.size && format == o.format;
    }
    bool operator<(const BufViewKey &o) const
    {
      if(buf != o.buf)
        return buf < o.buf;
      if(offs != o.offs)
        return offs < o.offs;
      if(size != o.size)
        return size < o.size;
      if(format != o.format)
        return format < o.format;
      return false;
    }
  };
  rdcflatmap<BufViewKey, VkBufferView> m_SampleBufViews;

  typedef rdcpair<ResourceId, float> SamplerBiasKey;
  std::map<SamplerBiasKey, VkSampler> m_BiasSamplers;

  Threading::RWLock bufferCacheLock;
  std::map<ShaderBindIndex, bytebuf> bufferCache;

  VkCommandBuffer queuedOpCmdBuffer = VK_NULL_HANDLE;
  VkDeviceSize mathOpResultOffset = 0;
  VkDeviceSize sampleGatherOpResultOffset = 0;
  uint32_t queueIndex = 0;
  const VkDeviceSize mathOpResultByteSize = sizeof(Vec4f) * 2;
  const VkDeviceSize sampleGatherOpResultByteSize = sizeof(Vec4f);
  const VkDeviceSize sampleGatherOpResultsStart =
      ShaderDebugData::MAX_QUEUED_OPS * mathOpResultByteSize;

  struct ImageData
  {
    uint32_t width = 0, height = 0, depth = 0;
    uint32_t texelSize = 0;
    uint64_t rowPitch = 0, slicePitch = 0, samplePitch = 0;
    ResourceFormat fmt;
    bytebuf bytes;

    byte *texel(const uint32_t *coord, uint32_t sample)
    {
      byte *ret = bytes.data();

      ret += samplePitch * sample;
      ret += slicePitch * coord[2];
      ret += rowPitch * coord[1];
      ret += texelSize * coord[0];

      return ret;
    }
  };

  Threading::RWLock imageCacheLock;
  std::map<ShaderBindIndex, ImageData> imageCache;

  const Descriptor &GetDescriptor(const rdcstr &access, const ShaderBindIndex &index, bool &valid)
  {
    CHECK_DEVICE_THREAD();
    static Descriptor dummy;

    if(index.category == DescriptorCategory::Unknown)
    {
      // invalid index, return a dummy data but don't mark as invalid
      return dummy;
    }

    int32_t a = m_Access.indexOf(index);

    // this should not happen unless the debugging references an array element that we didn't
    // detect dynamically. We could improve this by retrieving a more conservative access set
    // internally so that all descriptors are 'accessed'
    if(a < 0)
    {
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning,
                                 StringFormat::Fmt("Internal error: Binding %s %u[%u] did not "
                                                   "exist in calculated descriptor access when %s.",
                                                   ToStr(index.category).c_str(), index.index,
                                                   index.arrayElement, access.c_str()));
      valid = false;
      return dummy;
    }

    return m_Descriptors[a];
  }

  const SamplerDescriptor &GetSamplerDescriptor(const rdcstr &access, const ShaderBindIndex &index,
                                                bool &valid)
  {
    CHECK_DEVICE_THREAD();
    static SamplerDescriptor dummy;

    if(index.category == DescriptorCategory::Unknown)
    {
      // invalid index, return a dummy data but don't mark as invalid
      return dummy;
    }

    int32_t a = m_Access.indexOf(index);

    // this should not happen unless the debugging references an array element that we didn't
    // detect dynamically. We could improve this by retrieving a more conservative access set
    // internally so that all descriptors are 'accessed'
    if(a < 0)
    {
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning,
                                 StringFormat::Fmt("Internal error: Binding %s %u[%u] did not "
                                                   "exist in calculated descriptor access when %s.",
                                                   ToStr(index.category).c_str(), index.index,
                                                   index.arrayElement, access.c_str()));
      valid = false;
      return dummy;
    }

    return m_SamplerDescriptors[a];
  }

  // Called from any thread
  ShaderBindIndex GenerateBufferBind(const uint64_t address, size_t &offs)
  {
    ResourceId id;
    uint64_t ptrOffs;
    m_pDriver->GetResIDFromAddr(address, id, ptrOffs);
    offs = size_t(ptrOffs);

    ShaderBindIndex bind;
    bind.arrayElement = (address - offs) & 0xFFFFFFFFU;

    return bind;
  }

  // Called from any thread
  bool IsBufferCached(const ShaderBindIndex &bind) override
  {
    SCOPED_READLOCK(bufferCacheLock);
    return bufferCache.find(bind) != bufferCache.end();
  }

  // Called from any thread
  bool IsBufferCached(uint64_t address) override
  {
    size_t offs = 0;
    ShaderBindIndex bind = GenerateBufferBind(address, offs);
    return IsBufferCached(bind);
  }

  // Called from any thread
  bytebuf *GetBufferDataFromCache(const ShaderBindIndex &bind, rdcspv::DeviceOpResult &opResult)
  {
    // Calling function responsible for acquiring bufferCache Read lock
    auto findIt = bufferCache.find(bind);
    if(findIt != bufferCache.end())
    {
      opResult = rdcspv::DeviceOpResult::Succeeded;
      return &findIt->second;
    }

    opResult = rdcspv::DeviceOpResult::Failed;

    // Not in the cache : populate must happen on the device thread
    if(!IsDeviceThread())
      opResult = rdcspv::DeviceOpResult::NeedsDevice;

    return NULL;
  }

  // Called from any thread
  bool BufferFunction(const ShaderBindIndex &bind, const std::function<void(bytebuf *data)> &func,
                      rdcspv::DeviceOpResult &opResult)
  {
    bool isCached = false;
    {
      SCOPED_READLOCK(bufferCacheLock);
      isCached = GetBufferDataFromCache(bind, opResult) != NULL;
      if(opResult == rdcspv::DeviceOpResult::NeedsDevice)
        return false;
    }

    if(!isCached)
    {
      // Add buffer data to the cache : cache should not be locked by this thread
      PopulateBuffer(bind);
    }

    {
      SCOPED_READLOCK(bufferCacheLock);
      bytebuf *result = GetBufferDataFromCache(bind, opResult);
      if(result)
      {
        // Guarantee the buffer cache readlock whilst the function is called
        func(result);
        return true;
      }

      RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Failed);
      opResult = rdcspv::DeviceOpResult::Failed;
      return false;
    }
  }

  // Called from any thread
  bool BufferFunction(uint64_t address, const std::function<void(bytebuf *data, size_t offset)> &func,
                      rdcspv::DeviceOpResult &opResult)
  {
    size_t offs = 0;
    ShaderBindIndex bind = GenerateBufferBind(address, offs);
    bool isCached = false;
    {
      SCOPED_READLOCK(bufferCacheLock);
      isCached = GetBufferDataFromCache(bind, opResult) != NULL;
      if(opResult == rdcspv::DeviceOpResult::NeedsDevice)
        return false;
    }

    if(!isCached)
    {
      // Add buffer data to the cache : cache should not be locked by this thread
      PopulateBuffer(address, bind);
    }

    {
      SCOPED_READLOCK(bufferCacheLock);
      bytebuf *result = GetBufferDataFromCache(bind, opResult);
      if(result)
      {
        // Guarantee the buffer cache readlock whilst the function is called
        func(result, offs);
        return true;
      }

      RDCASSERTEQUAL(opResult, rdcspv::DeviceOpResult::Failed);
      opResult = rdcspv::DeviceOpResult::Failed;
      return false;
    }
  }

  // Must be called from the replay manager thread (the debugger thread)
  void PopulateBuffer(uint64_t address, ShaderBindIndex bind)
  {
    CHECK_DEVICE_THREAD();
    // pick a non-overlapping bind namespace for direct pointer access
    ResourceId id;
    uint64_t ptrOffs;

    m_pDriver->GetResIDFromAddr(address, id, ptrOffs);
    if(id == ResourceId())
    {
      ShaderBindIndex noBind;
      CHECK_DEVICE_THREAD();
      auto insertIt = bufferCache.insert(std::make_pair(noBind, bytebuf()));
      m_pDriver->AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                                 MessageSource::RuntimeWarning,
                                 StringFormat::Fmt("invalid or OOB pointer access detected ."));
      return;
    }

    // if the resources might be dirty from side-effects from the action, replay back to right
    // before it.
    if(m_ResourcesDirty)
    {
      VkMarkerRegion region("un-dirtying resources");
      m_pDriver->ReplayLog(0, m_EventID, eReplay_WithoutDraw);
      m_ResourcesDirty = false;
    }

    bytebuf data;
    m_pDriver->GetDebugManager()->GetBufferData(id, 0, 0, data);

    {
      // Insert atomically with all the data filled in : to prevent race conditions
      SCOPED_WRITELOCK(bufferCacheLock);
      auto insertIt = bufferCache.insert(std::make_pair(bind, data));
      RDCASSERT(insertIt.second);
    }
  }

  // Must be called from the replay manager thread (the debugger thread)
  void PopulateBuffer(const ShaderBindIndex &bind)
  {
    CHECK_DEVICE_THREAD();
    bytebuf data;

    bool valid = true;
    const Descriptor &bufData = GetDescriptor("accessing buffer value", bind, valid);
    if(valid)
    {
      // if the resources might be dirty from side-effects from the action, replay back to right
      // before it.
      if(m_ResourcesDirty)
      {
        VkMarkerRegion region("un-dirtying resources");
        m_pDriver->ReplayLog(0, m_EventID, eReplay_WithoutDraw);
        m_ResourcesDirty = false;
      }

      if(bufData.resource != ResourceId())
      {
        m_pDriver->GetReplay()->GetBufferData(bufData.resource, bufData.byteOffset,
                                              bufData.byteSize, data);
      }
    }

    {
      // Insert atomically with all the data filled in : to prevent race conditions
      SCOPED_WRITELOCK(bufferCacheLock);
      auto insertIt = bufferCache.insert(std::make_pair(bind, data));
      RDCASSERT(insertIt.second);
    }
  }

  // Must be called from the replay manager thread (the debugger thread)
  void PopulateImage(const ShaderBindIndex &bind)
  {
    CHECK_DEVICE_THREAD();

    ImageData data;
    bool valid = true;
    const Descriptor &imgData = GetDescriptor("performing image load/store", bind, valid);
    if(valid)
    {
      // if the resources might be dirty from side-effects from the action, replay back to right
      // before it.
      if(m_ResourcesDirty)
      {
        VkMarkerRegion region("un-dirtying resources");
        m_pDriver->ReplayLog(0, m_EventID, eReplay_WithoutDraw);
        m_ResourcesDirty = false;
      }

      if(imgData.type == DescriptorType::TypedBuffer ||
         imgData.type == DescriptorType::ReadWriteTypedBuffer)
      {
        VkFormat format;
        VkDeviceSize byteWidth;
        VkDeviceSize offset;
        ResourceId buffer;

        if(imgData.view == ResourceId())
        {
          // descriptor buffer, no buffer view
          buffer = imgData.resource;
          offset = imgData.byteOffset;
          format = MakeVkFormat(imgData.format);
          byteWidth = imgData.byteSize;
        }
        else
        {
          const VulkanCreationInfo::BufferView &viewProps =
              m_Creation.GetBufferViewInfo(imgData.view);
          buffer = viewProps.buffer;
          offset = viewProps.offset;
          format = viewProps.format;
          byteWidth = viewProps.size;
        }

        VulkanCreationInfo::Buffer defaultBufferProps = {};
        const VulkanCreationInfo::Buffer &bufferProps =
            (buffer == ResourceId()) ? defaultBufferProps : m_Creation.GetBufferInfo(buffer);

        // width in bytes, either from the view or from the remainder of the buffer
        if(byteWidth == VK_WHOLE_SIZE)
          byteWidth = bufferProps.size - offset;

        data.fmt = MakeResourceFormat(format);
        data.texelSize = (uint32_t)GetByteSize(1, 1, 1, format, 0);

        // convert to a texel width, rounding down as per spec (only possible from VK_WHOLE_SIZE)
        data.width = uint32_t(byteWidth / data.texelSize);
        data.height = 1;
        data.depth = 1;

        data.samplePitch = data.slicePitch = data.rowPitch = data.width * data.texelSize;

        m_pDriver->GetReplay()->GetBufferData(imgData.resource, offset, data.rowPitch, data.bytes);
      }
      else if(imgData.view != ResourceId())
      {
        const VulkanCreationInfo::ImageView &viewProps = m_Creation.GetImageViewInfo(imgData.view);
        if(viewProps.image != ResourceId())
        {
          const VulkanCreationInfo::Image &imageProps = m_Creation.GetImageInfo(viewProps.image);

          uint32_t mip = viewProps.range.baseMipLevel;

          data.width = RDCMAX(1U, imageProps.extent.width >> mip);
          data.height = RDCMAX(1U, imageProps.extent.height >> mip);
          if(imageProps.type == VK_IMAGE_TYPE_3D)
          {
            data.depth = RDCMAX(1U, imageProps.extent.depth >> mip);
          }
          else
          {
            data.depth = viewProps.range.layerCount;
            if(data.depth == VK_REMAINING_ARRAY_LAYERS)
              data.depth = imageProps.arrayLayers - viewProps.range.baseArrayLayer;
          }

          ResourceFormat fmt = MakeResourceFormat(imageProps.format);

          data.fmt = MakeResourceFormat(imageProps.format);
          data.texelSize = (uint32_t)GetByteSize(1, 1, 1, imageProps.format, 0);
          data.rowPitch = (uint32_t)GetByteSize(data.width, 1, 1, imageProps.format, 0);
          data.slicePitch = GetByteSize(data.width, data.height, 1, imageProps.format, 0);
          data.samplePitch = GetByteSize(data.width, data.height, data.depth, imageProps.format, 0);

          const uint32_t numSlices = imageProps.type == VK_IMAGE_TYPE_3D ? 1 : data.depth;
          const uint32_t numSamples = (uint32_t)imageProps.samples;

          data.bytes.reserve(size_t(data.samplePitch * numSamples));

          // defaults are fine - no interpretation. Maybe we could use the view's typecast?
          const GetTextureDataParams params = GetTextureDataParams();

          for(uint32_t sample = 0; sample < numSamples; sample++)
          {
            for(uint32_t slice = 0; slice < numSlices; slice++)
            {
              bytebuf subBytes;
              m_pDriver->GetReplay()->GetTextureData(
                  viewProps.image, Subresource(mip, slice, sample), params, subBytes);

              // fast path, swap into output if there's only one slice and one sample (common case)
              if(numSlices == 1 && numSamples == 1)
              {
                subBytes.swap(data.bytes);
              }
              else
              {
                data.bytes.append(subBytes);
              }
            }
          }
        }
      }
    }

    {
      // Insert atomically with all the data filled in : to prevent race conditions
      SCOPED_WRITELOCK(imageCacheLock);
      auto insertIt = imageCache.insert(std::make_pair(bind, data));
      RDCASSERT(insertIt.second);
    }
  }

  // Called from any thread
  bool IsImageCached(const ShaderBindIndex &bind) override
  {
    SCOPED_READLOCK(imageCacheLock);
    return imageCache.find(bind) != imageCache.end();
  }

  // Called from any thread
  ImageData *GetImageDataFromCache(const ShaderBindIndex &bind, rdcspv::DeviceOpResult &opResult)
  {
    // Calling function responsible for acquiring imageCache Read lock
    auto findIt = imageCache.find(bind);
    if(findIt != imageCache.end())
    {
      opResult = rdcspv::DeviceOpResult::Succeeded;
      return &findIt->second;
    }

    opResult = rdcspv::DeviceOpResult::Failed;

    // Not in the cache : populate must happen on the device thread
    if(!IsDeviceThread())
      opResult = rdcspv::DeviceOpResult::NeedsDevice;

    return NULL;
  }

  VkPipeline MakePipe(const ShaderConstParameters &params, uint32_t floatBitSize, bool depthTex,
                      bool uintTex, bool sintTex)
  {
    CHECK_DEVICE_THREAD();
    VkSpecializationMapEntry specMaps[sizeof(params) / sizeof(uint32_t)];
    for(size_t i = 0; i < ARRAY_COUNT(specMaps); i++)
    {
      specMaps[i].constantID = uint32_t(i);
      specMaps[i].offset = uint32_t(sizeof(uint32_t) * i);
      specMaps[i].size = sizeof(uint32_t);
    }

    VkSpecializationInfo specInfo = {};
    specInfo.dataSize = sizeof(params);
    specInfo.pData = &params;
    specInfo.mapEntryCount = ARRAY_COUNT(specMaps);
    specInfo.pMapEntries = specMaps;

    uint32_t shaderIndex = 0;
    if(depthTex)
      shaderIndex = 1;
    if(uintTex)
      shaderIndex = 2;
    else if(sintTex)
      shaderIndex = 3;

    if(params.operation == (uint32_t)rdcspv::Op::ExtInst)
    {
      shaderIndex = 4;
      if(floatBitSize == 16)
        shaderIndex = 5;
      else if(floatBitSize == 64)
        shaderIndex = 6;
    }

    if(m_DebugData.Module[shaderIndex] == VK_NULL_HANDLE)
    {
      rdcarray<uint32_t> spirv;

      if(params.operation == (uint32_t)rdcspv::Op::ExtInst)
      {
        GenerateMathShaderModule(spirv, floatBitSize);
      }
      else
      {
        RDCASSERTMSG("Assume sampling happens with 32-bit float inputs", floatBitSize == 32,
                     floatBitSize);
        GenerateSamplingShaderModule(spirv, depthTex, uintTex, sintTex);
      }

      VkShaderModuleCreateInfo moduleCreateInfo = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
      moduleCreateInfo.pCode = spirv.data();
      moduleCreateInfo.codeSize = spirv.size() * sizeof(uint32_t);

      VkResult vkr = m_pDriver->vkCreateShaderModule(m_pDriver->GetDev(), &moduleCreateInfo, NULL,
                                                     &m_DebugData.Module[shaderIndex]);
      CHECK_VKR(m_pDriver, vkr);

      const char *filename[] = {
          "/debug_psgather_float.spv", "/debug_psgather_depth.spv", "/debug_psgather_uint.spv",
          "/debug_psgather_sint.spv",  "/debug_psmath32.spv",       "/debug_psmath16.spv",
          "/debug_psmath64.spv",
      };

      if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
        FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + filename[shaderIndex], spirv);
    }

    uint32_t key = params.hashKey(shaderIndex);

    if(m_DebugData.m_Pipelines[key] != VK_NULL_HANDLE)
      return m_DebugData.m_Pipelines[key];

    RDCLOG(
        "Making new pipeline for shader type %u, operation %s, dim %u, useGrad/useGatherOffsets %u,"
        " gather channel %u, gather offsets...",
        shaderIndex, ToStr((rdcspv::Op)params.operation).c_str(), params.dim,
        params.useGradOrGatherOffsets, params.gatherChannel);

    if(params.operation == (uint32_t)rdcspv::Op::ExtInst)
    {
      VkComputePipelineCreateInfo computePipeInfo = {
          VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
          NULL,
          0,
          {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0,
           VK_SHADER_STAGE_COMPUTE_BIT, m_DebugData.Module[shaderIndex], "main", NULL},
          m_DebugData.PipeLayout,
          VK_NULL_HANDLE,
          0,
      };

      VkPipeline pipe = VK_NULL_HANDLE;
      VkResult vkr = m_pDriver->vkCreateComputePipelines(m_pDriver->GetDev(),
                                                         m_pDriver->GetShaderCache()->GetPipeCache(),
                                                         1, &computePipeInfo, NULL, &pipe);
      if(vkr != VK_SUCCESS)
      {
        RDCERR("Failed creating debug pipeline: %s", ToStr(vkr).c_str());
        return VK_NULL_HANDLE;
      }

      m_DebugData.m_Pipelines[key] = pipe;

      return pipe;
    }

    const VkPipelineShaderStageCreateInfo shaderStages[2] = {
        {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_VERTEX_BIT,
         m_pDriver->GetShaderCache()->GetBuiltinModule(BuiltinShader::ShaderDebugSampleVS), "main",
         NULL},
        {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, NULL, 0, VK_SHADER_STAGE_FRAGMENT_BIT,
         m_DebugData.Module[shaderIndex], "main", &specInfo},
    };

    const VkPipelineDynamicStateCreateInfo dynamicState = {
        VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
    };

    const VkPipelineMultisampleStateCreateInfo msaa = {
        VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
        NULL,
        0,
        VK_SAMPLE_COUNT_1_BIT,
    };

    const VkPipelineDepthStencilStateCreateInfo depthStencil = {
        VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
    };

    VkPipelineColorBlendAttachmentState colAttach = {};
    colAttach.colorWriteMask = 0xf;

    const VkPipelineColorBlendStateCreateInfo colorBlend = {
        VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
        NULL,
        0,
        false,
        VK_LOGIC_OP_NO_OP,
        1,
        &colAttach,
    };

    const VkPipelineVertexInputStateCreateInfo vertexInput = {
        VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
    };

    VkPipelineInputAssemblyStateCreateInfo inputAssembly = {
        VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
    };

    inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;

    VkRect2D s = {};
    s.extent.width = s.extent.height = 1;

    VkViewport v = {};
    v.width = v.height = v.maxDepth = 1.0f;

    VkPipelineViewportStateCreateInfo viewScissor = {
        VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
    };
    viewScissor.viewportCount = viewScissor.scissorCount = 1;
    viewScissor.pScissors = &s;
    viewScissor.pViewports = &v;

    VkPipelineRasterizationStateCreateInfo raster = {
        VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
    };

    raster.lineWidth = 1.0f;

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

    VkPipeline pipe = VK_NULL_HANDLE;
    VkResult vkr = m_pDriver->vkCreateGraphicsPipelines(m_pDriver->GetDev(),
                                                        m_pDriver->GetShaderCache()->GetPipeCache(),
                                                        1, &graphicsPipeInfo, NULL, &pipe);
    if(vkr != VK_SUCCESS)
    {
      RDCERR("Failed creating debug pipeline: %s", ToStr(vkr).c_str());
      return VK_NULL_HANDLE;
    }

    m_DebugData.m_Pipelines[key] = pipe;

    return pipe;
  }

  void GenerateMathShaderModule(rdcarray<uint32_t> &spirv, uint32_t floatBitSize)
  {
    CHECK_DEVICE_THREAD();
    rdcspv::Editor editor(spirv);

    // create as SPIR-V 1.0 for best compatibility
    editor.CreateEmpty(1, 0);

    editor.AddCapability(rdcspv::Capability::Shader);

    rdcspv::Id entryId = editor.MakeId();

    editor.AddOperation(
        editor.Begin(rdcspv::Section::MemoryModel),
        rdcspv::OpMemoryModel(rdcspv::AddressingModel::Logical, rdcspv::MemoryModel::GLSL450));

    rdcspv::Id glsl450 = editor.ImportExtInst("GLSL.std.450");

    rdcspv::Scalar sizedScalar;
    if(floatBitSize == 32)
      sizedScalar = rdcspv::scalar<float>();
    else if(floatBitSize == 64)
      sizedScalar = rdcspv::scalar<double>();
    else
      sizedScalar = rdcspv::scalar<half_float::half>();

    rdcspv::Id u32 = editor.DeclareType(rdcspv::scalar<uint32_t>());
    rdcspv::Id floatType = editor.DeclareType(sizedScalar);
    rdcspv::Id vec4Type = editor.DeclareType(rdcspv::Vector(sizedScalar, 4));

    rdcspv::Id pushStructID =
        editor.AddType(rdcspv::OpTypeStruct(editor.MakeId(), {vec4Type, vec4Type, vec4Type, u32}));
    editor.AddDecoration(rdcspv::OpDecorate(pushStructID, rdcspv::Decoration::Block));
    editor.AddDecoration(rdcspv::OpMemberDecorate(
        pushStructID, 0, rdcspv::DecorationParam<rdcspv::Decoration::Offset>(0)));
    editor.AddDecoration(rdcspv::OpMemberDecorate(
        pushStructID, 1, rdcspv::DecorationParam<rdcspv::Decoration::Offset>(sizeof(Vec4f) * 2)));
    editor.AddDecoration(rdcspv::OpMemberDecorate(
        pushStructID, 2, rdcspv::DecorationParam<rdcspv::Decoration::Offset>(sizeof(Vec4f) * 4)));
    editor.AddDecoration(rdcspv::OpMemberDecorate(
        pushStructID, 3, rdcspv::DecorationParam<rdcspv::Decoration::Offset>(sizeof(Vec4f) * 6)));
    editor.SetMemberName(pushStructID, 0, "a");
    editor.SetMemberName(pushStructID, 1, "b");
    editor.SetMemberName(pushStructID, 2, "c");
    editor.SetMemberName(pushStructID, 3, "op");

    rdcspv::Id pushPtrType =
        editor.DeclareType(rdcspv::Pointer(pushStructID, rdcspv::StorageClass::PushConstant));
    rdcspv::Id pushVar = editor.AddVariable(
        rdcspv::OpVariable(pushPtrType, editor.MakeId(), rdcspv::StorageClass::PushConstant));
    editor.SetName(pushVar, "pushData");

    rdcspv::Id pushv4Type =
        editor.DeclareType(rdcspv::Pointer(vec4Type, rdcspv::StorageClass::PushConstant));
    rdcspv::Id pushu32Type =
        editor.DeclareType(rdcspv::Pointer(u32, rdcspv::StorageClass::PushConstant));

    rdcspv::Id storageStructType = editor.AddType(rdcspv::OpTypeStruct(editor.MakeId(), {vec4Type}));
    editor.AddDecoration(rdcspv::OpMemberDecorate(
        storageStructType, 0, rdcspv::DecorationParam<rdcspv::Decoration::Offset>(0)));
    editor.DecorateStorageBufferStruct(storageStructType);

    rdcspv::Id storageStructPtrType =
        editor.DeclareType(rdcspv::Pointer(storageStructType, editor.StorageBufferClass()));
    rdcspv::Id storageVec4PtrType =
        editor.DeclareType(rdcspv::Pointer(vec4Type, editor.StorageBufferClass()));

    rdcspv::Id storageVar = editor.AddVariable(
        rdcspv::OpVariable(storageStructPtrType, editor.MakeId(), editor.StorageBufferClass()));
    editor.AddDecoration(rdcspv::OpDecorate(
        storageVar, rdcspv::DecorationParam<rdcspv::Decoration::DescriptorSet>(0U)));
    editor.AddDecoration(rdcspv::OpDecorate(
        storageVar,
        rdcspv::DecorationParam<rdcspv::Decoration::Binding>((uint32_t)ShaderDebugBind::MathResult)));

    editor.SetName(storageVar, "resultStorage");

    // register the entry point
    editor.AddOperation(editor.Begin(rdcspv::Section::EntryPoints),
                        rdcspv::OpEntryPoint(rdcspv::ExecutionModel::GLCompute, entryId, "main", {}));
    editor.AddExecutionMode(rdcspv::OpExecutionMode(
        entryId, rdcspv::ExecutionModeParam<rdcspv::ExecutionMode::LocalSize>(1, 1, 1)));

    rdcspv::Id voidType = editor.DeclareType(rdcspv::scalar<void>());
    rdcspv::Id funcType = editor.DeclareType(rdcspv::FunctionType(voidType, {}));

    rdcspv::OperationList func;
    func.add(rdcspv::OpFunction(voidType, entryId, rdcspv::FunctionControl::None, funcType));
    func.add(rdcspv::OpLabel(editor.MakeId()));

    rdcspv::Id consts[] = {
        editor.AddConstantImmediate<uint32_t>(0),
        editor.AddConstantImmediate<uint32_t>(1),
        editor.AddConstantImmediate<uint32_t>(2),
        editor.AddConstantImmediate<uint32_t>(3),
    };

    rdcspv::Id zerof;
    if(floatBitSize == 32)
      zerof = editor.AddConstantImmediate<float>(0.0f);
    else if(floatBitSize == 64)
      zerof = editor.AddConstantImmediate<double>(0.0);
    else
      zerof = editor.AddConstantImmediate<half_float::half>(half_float::half(0.0f));

    // load the parameters and the op
    rdcspv::Id aPtr =
        func.add(rdcspv::OpAccessChain(pushv4Type, editor.MakeId(), pushVar, {consts[0]}));
    rdcspv::Id bPtr =
        func.add(rdcspv::OpAccessChain(pushv4Type, editor.MakeId(), pushVar, {consts[1]}));
    rdcspv::Id cPtr =
        func.add(rdcspv::OpAccessChain(pushv4Type, editor.MakeId(), pushVar, {consts[2]}));
    rdcspv::Id opPtr =
        func.add(rdcspv::OpAccessChain(pushu32Type, editor.MakeId(), pushVar, {consts[3]}));
    rdcspv::Id a = func.add(rdcspv::OpLoad(vec4Type, editor.MakeId(), aPtr));
    rdcspv::Id b = func.add(rdcspv::OpLoad(vec4Type, editor.MakeId(), bPtr));
    rdcspv::Id c = func.add(rdcspv::OpLoad(vec4Type, editor.MakeId(), cPtr));
    rdcspv::Id opParam = func.add(rdcspv::OpLoad(u32, editor.MakeId(), opPtr));

    // access chain the output
    rdcspv::Id outVar =
        func.add(rdcspv::OpAccessChain(storageVec4PtrType, editor.MakeId(), storageVar, {consts[0]}));

    rdcspv::Id breakLabel = editor.MakeId();
    rdcspv::Id defaultLabel = editor.MakeId();

    rdcarray<rdcspv::SwitchPairU32LiteralId> targets;

    rdcspv::OperationList cases;

    // all these operations take one parameter and only operate on floats (possibly vectors)
    for(rdcspv::GLSLstd450 op : {
            rdcspv::GLSLstd450::Sin,       rdcspv::GLSLstd450::Cos,
            rdcspv::GLSLstd450::Tan,       rdcspv::GLSLstd450::Asin,
            rdcspv::GLSLstd450::Acos,      rdcspv::GLSLstd450::Atan,
            rdcspv::GLSLstd450::Sinh,      rdcspv::GLSLstd450::Cosh,
            rdcspv::GLSLstd450::Tanh,      rdcspv::GLSLstd450::Asinh,
            rdcspv::GLSLstd450::Acosh,     rdcspv::GLSLstd450::Atanh,
            rdcspv::GLSLstd450::Exp,       rdcspv::GLSLstd450::Log,
            rdcspv::GLSLstd450::Exp2,      rdcspv::GLSLstd450::Log2,
            rdcspv::GLSLstd450::Sqrt,      rdcspv::GLSLstd450::InverseSqrt,
            rdcspv::GLSLstd450::Normalize,
        })
    {
      // most operations aren't allowed on doubles
      if(floatBitSize == 64 && op != rdcspv::GLSLstd450::Sqrt &&
         op != rdcspv::GLSLstd450::InverseSqrt && op != rdcspv::GLSLstd450::Normalize)
        continue;

      rdcspv::Id label = editor.MakeId();
      targets.push_back({(uint32_t)op, label});

      cases.add(rdcspv::OpLabel(label));
      rdcspv::Id result = cases.add(rdcspv::OpGLSL450(vec4Type, editor.MakeId(), glsl450, op, {a}));
      cases.add(rdcspv::OpStore(outVar, result));
      cases.add(rdcspv::OpBranch(breakLabel));
    }

    if(floatBitSize != 64)
    {
      // these take two parameters, but are otherwise identical
      for(rdcspv::GLSLstd450 op : {rdcspv::GLSLstd450::Atan2, rdcspv::GLSLstd450::Pow})
      {
        rdcspv::Id label = editor.MakeId();
        targets.push_back({(uint32_t)op, label});

        cases.add(rdcspv::OpLabel(label));
        rdcspv::Id result =
            cases.add(rdcspv::OpGLSL450(vec4Type, editor.MakeId(), glsl450, op, {a, b}));
        cases.add(rdcspv::OpStore(outVar, result));
        cases.add(rdcspv::OpBranch(breakLabel));
      }
    }

    {
      rdcspv::GLSLstd450 op = rdcspv::GLSLstd450::Fma;

      rdcspv::Id label = editor.MakeId();
      targets.push_back({(uint32_t)op, label});

      cases.add(rdcspv::OpLabel(label));
      rdcspv::Id result =
          cases.add(rdcspv::OpGLSL450(vec4Type, editor.MakeId(), glsl450, op, {a, b, c}));
      cases.add(rdcspv::OpStore(outVar, result));
      cases.add(rdcspv::OpBranch(breakLabel));
    }

    // these ones are special
    {
      rdcspv::GLSLstd450 op = rdcspv::GLSLstd450::Length;

      rdcspv::Id label = editor.MakeId();
      targets.push_back({(uint32_t)op, label});

      cases.add(rdcspv::OpLabel(label));
      rdcspv::Id result = cases.add(rdcspv::OpGLSL450(floatType, editor.MakeId(), glsl450, op, {a}));
      rdcspv::Id resultvec = cases.add(
          rdcspv::OpCompositeConstruct(vec4Type, editor.MakeId(), {result, zerof, zerof, zerof}));
      cases.add(rdcspv::OpStore(outVar, resultvec));
      cases.add(rdcspv::OpBranch(breakLabel));
    }

    {
      rdcspv::GLSLstd450 op = rdcspv::GLSLstd450::Distance;

      rdcspv::Id label = editor.MakeId();
      targets.push_back({(uint32_t)op, label});

      cases.add(rdcspv::OpLabel(label));
      rdcspv::Id result =
          cases.add(rdcspv::OpGLSL450(floatType, editor.MakeId(), glsl450, op, {a, b}));
      rdcspv::Id resultvec = cases.add(
          rdcspv::OpCompositeConstruct(vec4Type, editor.MakeId(), {result, zerof, zerof, zerof}));
      cases.add(rdcspv::OpStore(outVar, resultvec));
      cases.add(rdcspv::OpBranch(breakLabel));
    }

    {
      rdcspv::GLSLstd450 op = rdcspv::GLSLstd450::Refract;

      rdcspv::Id label = editor.MakeId();
      targets.push_back({(uint32_t)op, label});

      cases.add(rdcspv::OpLabel(label));
      rdcspv::Id eta = cases.add(rdcspv::OpCompositeExtract(floatType, editor.MakeId(), c, {0}));
      rdcspv::Id result =
          cases.add(rdcspv::OpGLSL450(vec4Type, editor.MakeId(), glsl450, op, {a, b, eta}));
      cases.add(rdcspv::OpStore(outVar, result));
      cases.add(rdcspv::OpBranch(breakLabel));
    }

    func.add(rdcspv::OpSelectionMerge(breakLabel, rdcspv::SelectionControl::None));
    func.add(rdcspv::OpSwitch32(opParam, defaultLabel, targets));

    func.append(cases);

    // default: store NULL data
    func.add(rdcspv::OpLabel(defaultLabel));
    func.add(rdcspv::OpStore(
        outVar, editor.AddConstant(rdcspv::OpConstantNull(vec4Type, editor.MakeId()))));
    func.add(rdcspv::OpBranch(breakLabel));

    func.add(rdcspv::OpLabel(breakLabel));
    func.add(rdcspv::OpReturn());
    func.add(rdcspv::OpFunctionEnd());

    editor.AddFunction(func);
  }

  void GenerateSamplingShaderModule(rdcarray<uint32_t> &spirv, bool depthTex, bool uintTex,
                                    bool sintTex)
  {
    CHECK_DEVICE_THREAD();
    // this could be done as a glsl shader, but glslang has some bugs compiling the specialisation
    // constants, so we generate it by hand - which isn't too hard

    rdcspv::Editor editor(spirv);

    // create as SPIR-V 1.0 for best compatibility
    editor.CreateEmpty(1, 0);

    editor.AddCapability(rdcspv::Capability::Shader);
    editor.AddCapability(rdcspv::Capability::ImageQuery);
    editor.AddCapability(rdcspv::Capability::Sampled1D);
    editor.AddCapability(rdcspv::Capability::SampledBuffer);

    if(m_pDriver->GetDeviceEnabledFeatures().shaderResourceMinLod)
      editor.AddCapability(rdcspv::Capability::MinLod);
    if(m_pDriver->GetDeviceEnabledFeatures().shaderImageGatherExtended)
      editor.AddCapability(rdcspv::Capability::ImageGatherExtended);

    const bool cubeArray = (m_pDriver->GetDeviceEnabledFeatures().imageCubeArray != VK_FALSE);

    rdcspv::Id entryId = editor.MakeId();

    editor.AddOperation(
        editor.Begin(rdcspv::Section::MemoryModel),
        rdcspv::OpMemoryModel(rdcspv::AddressingModel::Logical, rdcspv::MemoryModel::GLSL450));

    rdcspv::Id u32 = editor.DeclareType(rdcspv::scalar<uint32_t>());
    rdcspv::Id i32 = editor.DeclareType(rdcspv::scalar<int32_t>());
    rdcspv::Id f32 = editor.DeclareType(rdcspv::scalar<float>());

    rdcspv::Id v2i32 = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<int32_t>(), 2));
    rdcspv::Id v3i32 = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<int32_t>(), 3));
    rdcspv::Id v2f32 = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 2));
    rdcspv::Id v3f32 = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 3));
    rdcspv::Id v4f32 = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 4));

    // int2[4]
    rdcspv::Id a4v2i32 = editor.AddType(
        rdcspv::OpTypeArray(editor.MakeId(), v2i32, editor.AddConstantImmediate<uint32_t>(4)));

    rdcspv::Scalar base = rdcspv::scalar<float>();
    if(uintTex)
      base = rdcspv::scalar<uint32_t>();
    else if(sintTex)
      base = rdcspv::scalar<int32_t>();

    rdcspv::Id resultType = editor.DeclareType(rdcspv::Vector(base, 4));
    rdcspv::Id scalarResultType = editor.DeclareType(base);

// add specialisation constants for all the parameters
#define MEMBER_IDX(struct, name) uint32_t(offsetof(struct, name) / sizeof(uint32_t))

#define DECL_SPECID(type, name, value)                                                     \
  rdcspv::Id name =                                                                        \
      editor.AddSpecConstantImmediate<type>(0U, MEMBER_IDX(ShaderConstParameters, value)); \
  editor.SetName(name, "spec_" #name);

    DECL_SPECID(uint32_t, operation, operation);
    DECL_SPECID(bool, useGradOrGatherOffsets, useGradOrGatherOffsets);
    DECL_SPECID(uint32_t, dim, dim);
    DECL_SPECID(int32_t, gatherChannel, gatherChannel);
    DECL_SPECID(int32_t, gather_u0, gatherOffsets.u0);
    DECL_SPECID(int32_t, gather_v0, gatherOffsets.v0);
    DECL_SPECID(int32_t, gather_u1, gatherOffsets.u1);
    DECL_SPECID(int32_t, gather_v1, gatherOffsets.v1);
    DECL_SPECID(int32_t, gather_u2, gatherOffsets.u2);
    DECL_SPECID(int32_t, gather_v2, gatherOffsets.v2);
    DECL_SPECID(int32_t, gather_u3, gatherOffsets.u3);
    DECL_SPECID(int32_t, gather_v3, gatherOffsets.v3);

    struct StructMember
    {
      rdcspv::Id loadedType;
      rdcspv::Id ptrType;
      rdcspv::Id loadedId;
      const char *name;
      uint32_t memberIndex;
    };

    rdcarray<rdcspv::Id> memberIds;
    rdcarray<StructMember> cbufferMembers;

    rdcspv::Id type_int32_t = editor.DeclareType(rdcspv::scalar<int32_t>());
    rdcspv::Id type_float = editor.DeclareType(rdcspv::scalar<float>());

    rdcspv::Id uniformptr_int32_t =
        editor.DeclareType(rdcspv::Pointer(type_int32_t, rdcspv::StorageClass::Uniform));
    rdcspv::Id uniformptr_float =
        editor.DeclareType(rdcspv::Pointer(type_float, rdcspv::StorageClass::Uniform));

#define DECL_UNIFORM(type, name, value)                                                  \
  rdcspv::Id name = editor.MakeId();                                                     \
  editor.SetName(name, "uniform_" #name);                                                \
  cbufferMembers.push_back({CONCAT(type_, type), CONCAT(uniformptr_, type), name, #name, \
                            MEMBER_IDX(ShaderUniformParameters, value)});                \
  memberIds.push_back(CONCAT(type_, type));
    DECL_UNIFORM(int32_t, texel_u, texel_uvw.x);
    DECL_UNIFORM(int32_t, texel_v, texel_uvw.y);
    DECL_UNIFORM(int32_t, texel_w, texel_uvw.z);
    DECL_UNIFORM(int32_t, texel_lod, texel_lod);
    DECL_UNIFORM(float, u, uvwa[0]);
    DECL_UNIFORM(float, v, uvwa[1]);
    DECL_UNIFORM(float, w, uvwa[2]);
    DECL_UNIFORM(float, cube_a, uvwa[3]);
    DECL_UNIFORM(float, dudx, ddx[0]);
    DECL_UNIFORM(float, dvdx, ddx[1]);
    DECL_UNIFORM(float, dwdx, ddx[2]);
    DECL_UNIFORM(float, dudy, ddy[0]);
    DECL_UNIFORM(float, dvdy, ddy[1]);
    DECL_UNIFORM(float, dwdy, ddy[2]);
    DECL_UNIFORM(int32_t, dynoffset_u, offset.x);
    DECL_UNIFORM(int32_t, dynoffset_v, offset.y);
    DECL_UNIFORM(int32_t, dynoffset_w, offset.z);
    DECL_UNIFORM(int32_t, sampleIdx, sampleIdx);
    DECL_UNIFORM(float, compare, compare);
    DECL_UNIFORM(float, lod, lod);
    DECL_UNIFORM(float, minlod, minlod);

    rdcspv::Id cbufferStructID = editor.AddType(rdcspv::OpTypeStruct(editor.MakeId(), memberIds));
    editor.AddDecoration(rdcspv::OpDecorate(cbufferStructID, rdcspv::Decoration::Block));
    for(const StructMember &m : cbufferMembers)
    {
      editor.AddDecoration(rdcspv::OpMemberDecorate(
          cbufferStructID, m.memberIndex,
          rdcspv::DecorationParam<rdcspv::Decoration::Offset>(m.memberIndex * sizeof(uint32_t))));
      editor.SetMemberName(cbufferStructID, m.memberIndex, m.name);
    }

    rdcspv::Id constoffset_u = gather_u0;
    rdcspv::Id constoffset_uv = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v2i32, editor.MakeId(), {gather_u0, gather_v0}));
    rdcspv::Id constoffset_uvw = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v3i32, editor.MakeId(), {gather_u0, gather_v0, gather_u1}));
    editor.SetName(constoffset_u, "constoffset_u");
    editor.SetName(constoffset_uv, "constoffset_uv");
    editor.SetName(constoffset_uvw, "constoffset_uvw");

    rdcspv::Id gather_0 = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v2i32, editor.MakeId(), {gather_u0, gather_v0}));
    rdcspv::Id gather_1 = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v2i32, editor.MakeId(), {gather_u1, gather_v1}));
    rdcspv::Id gather_2 = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v2i32, editor.MakeId(), {gather_u2, gather_v2}));
    rdcspv::Id gather_3 = editor.AddConstant(
        rdcspv::OpSpecConstantComposite(v2i32, editor.MakeId(), {gather_u3, gather_v3}));

    rdcspv::Id gatherOffsets = editor.AddConstant(rdcspv::OpSpecConstantComposite(
        a4v2i32, editor.MakeId(), {gather_0, gather_1, gather_2, gather_3}));

    editor.SetName(gatherOffsets, "gatherOffsets");

    // create the output. It's always a 4-wide vector
    rdcspv::Id outPtrType =
        editor.DeclareType(rdcspv::Pointer(resultType, rdcspv::StorageClass::Output));

    rdcspv::Id outVar = editor.AddVariable(
        rdcspv::OpVariable(outPtrType, editor.MakeId(), rdcspv::StorageClass::Output));
    editor.AddDecoration(
        rdcspv::OpDecorate(outVar, rdcspv::DecorationParam<rdcspv::Decoration::Location>(0)));

    editor.SetName(outVar, "output");

    rdcspv::ImageFormat unk = rdcspv::ImageFormat::Unknown;

    // create the five textures and sampler
    rdcspv::Id texSampTypes[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::_1D, 0, 1, 0, 1, unk)),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::_2D, 0, 1, 0, 1, unk)),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::_3D, 0, 0, 0, 1, unk)),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::_2D, 0, 1, 1, 1, unk)),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::Cube, 0, cubeArray ? 1 : 0, 0, 1, unk)),
        editor.DeclareType(rdcspv::Image(base, rdcspv::Dim::Buffer, 0, 0, 0, 1, unk)),
        editor.DeclareType(rdcspv::Sampler()),
        cbufferStructID,
    };
    rdcspv::Id bindVars[(uint32_t)ShaderDebugBind::Count];
    rdcspv::Id texSampCombinedTypes[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[1])),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[2])),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[3])),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[4])),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[5])),
        editor.DeclareType(rdcspv::SampledImage(texSampTypes[6])),
        rdcspv::Id(),
        rdcspv::Id(),
    };

    for(size_t i = (size_t)ShaderDebugBind::First; i < (size_t)ShaderDebugBind::Count; i++)
    {
      rdcspv::StorageClass storageClass = rdcspv::StorageClass::UniformConstant;

      if(depthTex)
      {
        if(i == (size_t)ShaderDebugBind::Tex3D && !m_pDriver->GetReplay()->Depth3DSupported())
          continue;
        else if(i == (size_t)ShaderDebugBind::TexCube && !m_pDriver->GetReplay()->DepthCubeSupported())
          continue;
      }

      if(i == (size_t)ShaderDebugBind::Constants)
        storageClass = rdcspv::StorageClass::Uniform;

      rdcspv::Id ptrType = editor.DeclareType(rdcspv::Pointer(texSampTypes[i], storageClass));

      bindVars[i] = editor.AddVariable(rdcspv::OpVariable(ptrType, editor.MakeId(), storageClass));

      editor.AddDecoration(rdcspv::OpDecorate(
          bindVars[i], rdcspv::DecorationParam<rdcspv::Decoration::DescriptorSet>(0U)));
      editor.AddDecoration(rdcspv::OpDecorate(
          bindVars[i], rdcspv::DecorationParam<rdcspv::Decoration::Binding>((uint32_t)i)));
    }

    editor.SetName(bindVars[(size_t)ShaderDebugBind::Tex1D], "Tex1D");
    editor.SetName(bindVars[(size_t)ShaderDebugBind::Tex2D], "Tex2D");
    if(bindVars[(size_t)ShaderDebugBind::Tex3D] != rdcspv::Id())
      editor.SetName(bindVars[(size_t)ShaderDebugBind::Tex3D], "Tex3D");
    editor.SetName(bindVars[(size_t)ShaderDebugBind::Tex2DMS], "Tex2DMS");
    if(bindVars[(size_t)ShaderDebugBind::TexCube] != rdcspv::Id())
      editor.SetName(bindVars[(size_t)ShaderDebugBind::TexCube], "TexCube");
    editor.SetName(bindVars[(size_t)ShaderDebugBind::Buffer], "Buffer");
    editor.SetName(bindVars[(size_t)ShaderDebugBind::Sampler], "Sampler");
    editor.SetName(bindVars[(size_t)ShaderDebugBind::Constants], "CBuffer");

    rdcspv::Id uvwa_ptr = editor.DeclareType(rdcspv::Pointer(v4f32, rdcspv::StorageClass::Input));
    rdcspv::Id input_uvwa_var = editor.AddVariable(
        rdcspv::OpVariable(uvwa_ptr, editor.MakeId(), rdcspv::StorageClass::Input));
    editor.AddDecoration(rdcspv::OpDecorate(
        input_uvwa_var, rdcspv::DecorationParam<rdcspv::Decoration::Location>(0)));

    editor.SetName(input_uvwa_var, "input_uvwa");

    // register the entry point
    editor.AddOperation(editor.Begin(rdcspv::Section::EntryPoints),
                        rdcspv::OpEntryPoint(rdcspv::ExecutionModel::Fragment, entryId, "main",
                                             {input_uvwa_var, outVar}));
    editor.AddExecutionMode(rdcspv::OpExecutionMode(entryId, rdcspv::ExecutionMode::OriginUpperLeft));

    rdcspv::Id voidType = editor.DeclareType(rdcspv::scalar<void>());
    rdcspv::Id funcType = editor.DeclareType(rdcspv::FunctionType(voidType, {}));

    rdcspv::OperationList func;
    func.add(rdcspv::OpFunction(voidType, entryId, rdcspv::FunctionControl::None, funcType));
    func.add(rdcspv::OpLabel(editor.MakeId()));

    // access chain and load all the cbuffer variables
    for(const StructMember &m : cbufferMembers)
    {
      rdcspv::Id ptr = func.add(rdcspv::OpAccessChain(
          m.ptrType, editor.MakeId(), bindVars[(size_t)ShaderDebugBind::Constants],
          {editor.AddConstantImmediate<uint32_t>(m.memberIndex)}));
      func.add(rdcspv::OpLoad(m.loadedType, m.loadedId, ptr));
    }

    // declare cbuffer composites
    rdcspv::Id texel_uv =
        func.add(rdcspv::OpCompositeConstruct(v2i32, editor.MakeId(), {texel_u, texel_v}));
    rdcspv::Id texel_uvw =
        func.add(rdcspv::OpCompositeConstruct(v3i32, editor.MakeId(), {texel_u, texel_v, texel_w}));

    editor.SetName(texel_uv, "texel_uv");
    editor.SetName(texel_uvw, "texel_uvw");

    rdcspv::Id uv = func.add(rdcspv::OpCompositeConstruct(v2f32, editor.MakeId(), {u, v}));
    rdcspv::Id uvw = func.add(rdcspv::OpCompositeConstruct(v3f32, editor.MakeId(), {u, v, w}));
    rdcspv::Id uvwa =
        func.add(rdcspv::OpCompositeConstruct(v4f32, editor.MakeId(), {u, v, w, cube_a}));

    editor.SetName(uv, "uv");
    editor.SetName(uvw, "uvw");
    editor.SetName(uvwa, "uvwa");

    rdcspv::Id ddx_uv = func.add(rdcspv::OpCompositeConstruct(v2f32, editor.MakeId(), {dudx, dvdx}));
    rdcspv::Id ddx_uvw =
        func.add(rdcspv::OpCompositeConstruct(v3f32, editor.MakeId(), {dudx, dvdx, dwdx}));

    editor.SetName(ddx_uv, "ddx_uv");
    editor.SetName(ddx_uvw, "ddx_uvw");

    rdcspv::Id ddy_uv = func.add(rdcspv::OpCompositeConstruct(v2f32, editor.MakeId(), {dudy, dvdy}));
    rdcspv::Id ddy_uvw =
        func.add(rdcspv::OpCompositeConstruct(v3f32, editor.MakeId(), {dudy, dvdy, dwdy}));

    editor.SetName(ddy_uv, "ddy_uv");
    editor.SetName(ddy_uvw, "ddy_uvw");

    rdcspv::Id dynoffset_uv =
        func.add(rdcspv::OpCompositeConstruct(v2i32, editor.MakeId(), {dynoffset_u, dynoffset_v}));
    rdcspv::Id dynoffset_uvw = func.add(rdcspv::OpCompositeConstruct(
        v3i32, editor.MakeId(), {dynoffset_u, dynoffset_v, dynoffset_w}));

    editor.SetName(dynoffset_uv, "dynoffset_uv");
    editor.SetName(dynoffset_uvw, "dynoffset_uvw");

    rdcspv::Id input_uvwa = func.add(rdcspv::OpLoad(v4f32, editor.MakeId(), input_uvwa_var));
    rdcspv::Id input_uvw =
        func.add(rdcspv::OpVectorShuffle(v3f32, editor.MakeId(), input_uvwa, input_uvwa, {0, 1, 2}));
    rdcspv::Id input_uv =
        func.add(rdcspv::OpVectorShuffle(v2f32, editor.MakeId(), input_uvw, input_uvw, {0, 1}));
    rdcspv::Id input_u = func.add(rdcspv::OpCompositeExtract(f32, editor.MakeId(), input_uvw, {0}));

    // first store NULL data in, so the output is always initialised

    rdcspv::Id breakLabel = editor.MakeId();
    rdcspv::Id defaultLabel = editor.MakeId();

    // combine the operation with the image type:
    // operation * 10 + dim
    RDCCOMPILE_ASSERT(size_t(ShaderDebugBind::Count) < 10, "Combining value ranges will overlap!");
    rdcspv::Id switchVal = func.add(rdcspv::OpIMul(u32, editor.MakeId(), operation,
                                                   editor.AddConstantImmediate<uint32_t>(10U)));
    switchVal = func.add(rdcspv::OpIAdd(u32, editor.MakeId(), switchVal, dim));

    // switch on the combined operation and image type value
    rdcarray<rdcspv::SwitchPairU32LiteralId> targets;

    rdcspv::OperationList cases;

    rdcspv::Id texel_coord[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        texel_uv,        // 1D - u and array
        texel_uvw,       // 2D - u,v and array
        texel_uvw,       // 3D - u,v,w
        texel_uvw,       // 2DMS - u,v and array
        rdcspv::Id(),    // Cube
        texel_u,         // Buffer - u
    };

    // only used for QueryLod, so we can ignore MSAA/Buffer
    rdcspv::Id input_coord[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        input_u,         // 1D - u
        input_uv,        // 2D - u,v
        input_uvw,       // 3D - u,v,w
        rdcspv::Id(),    // 2DMS
        input_uvw,       // Cube - u,v,w
        rdcspv::Id(),    // Buffer
    };

    rdcspv::Id coord[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        uv,                        // 1D - u and array
        uvw,                       // 2D - u,v and array
        uvw,                       // 3D - u,v,w
        uvw,                       // 2DMS - u,v and array
        cubeArray ? uvwa : uvw,    // Cube - u,v,w and array (if supported)
        u,                         // Buffer - u
    };

    rdcspv::Id constoffset[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        constoffset_u,      // 1D - u
        constoffset_uv,     // 2D - u,v
        constoffset_uvw,    // 3D - u,v,w
        constoffset_uv,     // 2DMS - u,v
        rdcspv::Id(),       // Cube - not valid
        constoffset_u,      // Buffer - u
    };

    rdcspv::Id dynoffset[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        dynoffset_u,      // 1D - u
        dynoffset_uv,     // 2D - u,v
        dynoffset_uvw,    // 3D - u,v,w
        dynoffset_uv,     // 2DMS - u,v
        rdcspv::Id(),     // Cube - not valid
        dynoffset_u,      // Buffer - u
    };

    rdcspv::Id ddxs[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        dudx,       // 1D - u
        ddx_uv,     // 2D - u,v
        ddx_uvw,    // 3D - u,v,w
        ddx_uv,     // 2DMS - u,v
        ddx_uvw,    // Cube - u,v,w
        dudx,       // Buffer - u
    };

    rdcspv::Id ddys[(uint32_t)ShaderDebugBind::Count] = {
        rdcspv::Id(),
        dudy,       // 1D - u
        ddy_uv,     // 2D - u,v
        ddy_uvw,    // 3D - u,v,w
        ddy_uv,     // 2DMS - u,v
        ddy_uvw,    // Cube - u,v,w
        dudy,       // Buffer - u
    };

    uint32_t sampIdx = (uint32_t)ShaderDebugBind::Sampler;

    rdcspv::Id zerof = editor.AddConstantImmediate<float>(0.0f);

    for(uint32_t i = (uint32_t)ShaderDebugBind::First; i < (uint32_t)ShaderDebugBind::Count; i++)
    {
      if(i == sampIdx || i == (uint32_t)ShaderDebugBind::Constants)
        continue;

      if(bindVars[i] == rdcspv::Id())
        continue;

      rdcspv::ImageOperandsAndParamDatas imageOperandsWithOffsets;

      // most operations support offsets, set the operands commonly here.
      // with the shaderImageGatherExtended feature, gather opcodes will always get their operands
      // via uniforms to cut down on pipeline specialisations a little, but all other cases the
      // offsets must be constant.
      if(constoffset[i] != rdcspv::Id())
        imageOperandsWithOffsets.setConstOffset(constoffset[i]);

      // can't fetch from cubemaps
      if(i != (uint32_t)ShaderDebugBind::TexCube)
      {
        rdcspv::Op op = rdcspv::Op::ImageFetch;

        rdcspv::Id label = editor.MakeId();
        targets.push_back({(uint32_t)op * 10 + i, label});

        rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;

        if(i != (uint32_t)ShaderDebugBind::Buffer && i != (uint32_t)ShaderDebugBind::Tex2DMS)
          operands.setLod(texel_lod);

        if(i == (uint32_t)ShaderDebugBind::Tex2DMS)
          operands.setSample(sampleIdx);

        cases.add(rdcspv::OpLabel(label));
        rdcspv::Id loaded = cases.add(rdcspv::OpLoad(texSampTypes[i], editor.MakeId(), bindVars[i]));
        rdcspv::Id sampleResult = cases.add(
            rdcspv::OpImageFetch(resultType, editor.MakeId(), loaded, texel_coord[i], operands));
        cases.add(rdcspv::OpStore(outVar, sampleResult));
        cases.add(rdcspv::OpBranch(breakLabel));
      }

      // buffers and multisampled images don't support sampling, so skip the other operations at
      // this point
      if(i == (uint32_t)ShaderDebugBind::Buffer || i == (uint32_t)ShaderDebugBind::Tex2DMS)
        continue;

      {
        rdcspv::Op op = rdcspv::Op::ImageQueryLod;

        rdcspv::Id label = editor.MakeId();
        targets.push_back({(uint32_t)op * 10 + i, label});

        cases.add(rdcspv::OpLabel(label));
        rdcspv::Id loadedImage =
            cases.add(rdcspv::OpLoad(texSampTypes[i], editor.MakeId(), bindVars[i]));
        rdcspv::Id loadedSampler =
            cases.add(rdcspv::OpLoad(texSampTypes[sampIdx], editor.MakeId(), bindVars[sampIdx]));

        rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
            texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

        rdcspv::Id sampleResult =
            cases.add(rdcspv::OpImageQueryLod(v2f32, editor.MakeId(), combined, input_coord[i]));
        sampleResult = cases.add(rdcspv::OpVectorShuffle(v4f32, editor.MakeId(), sampleResult,
                                                         sampleResult, {0, 1, 0, 1}));

        // if we're sampling from an integer texture the output variable will be the same type.
        // Just bitcast the float bits into it, which will come out the other side the right type.
        if(uintTex || sintTex)
          sampleResult = cases.add(rdcspv::OpBitcast(resultType, editor.MakeId(), sampleResult));

        cases.add(rdcspv::OpStore(outVar, sampleResult));
        cases.add(rdcspv::OpBranch(breakLabel));
      }

      for(rdcspv::Op op : {rdcspv::Op::ImageSampleExplicitLod, rdcspv::Op::ImageSampleImplicitLod})
      {
        rdcspv::Id label = editor.MakeId();
        targets.push_back({(uint32_t)op * 10 + i, label});

        cases.add(rdcspv::OpLabel(label));
        rdcspv::Id loadedImage =
            cases.add(rdcspv::OpLoad(texSampTypes[i], editor.MakeId(), bindVars[i]));
        rdcspv::Id loadedSampler =
            cases.add(rdcspv::OpLoad(texSampTypes[sampIdx], editor.MakeId(), bindVars[sampIdx]));

        rdcspv::Id mergeLabel = editor.MakeId();
        rdcspv::Id gradCase = editor.MakeId();
        rdcspv::Id lodCase = editor.MakeId();
        cases.add(rdcspv::OpSelectionMerge(mergeLabel, rdcspv::SelectionControl::None));
        cases.add(rdcspv::OpBranchConditional(useGradOrGatherOffsets, gradCase, lodCase));

        rdcspv::Id lodResult;
        {
          cases.add(rdcspv::OpLabel(lodCase));
          rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;
          operands.setLod(lod);
          rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
              texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

          lodResult = cases.add(rdcspv::OpImageSampleExplicitLod(resultType, editor.MakeId(),
                                                                 combined, coord[i], operands));

          cases.add(rdcspv::OpBranch(mergeLabel));
        }

        rdcspv::Id gradResult;
        {
          cases.add(rdcspv::OpLabel(gradCase));
          rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;
          operands.setGrad(ddxs[i], ddys[i]);
          if(m_pDriver->GetDeviceEnabledFeatures().shaderResourceMinLod)
            operands.setMinLod(minlod);
          rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
              texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

          gradResult = cases.add(rdcspv::OpImageSampleExplicitLod(resultType, editor.MakeId(),
                                                                  combined, coord[i], operands));

          cases.add(rdcspv::OpBranch(mergeLabel));
        }

        cases.add(rdcspv::OpLabel(mergeLabel));
        rdcspv::Id sampleResult = cases.add(rdcspv::OpPhi(
            resultType, editor.MakeId(), {{lodResult, lodCase}, {gradResult, gradCase}}));
        cases.add(rdcspv::OpStore(outVar, sampleResult));
        cases.add(rdcspv::OpBranch(breakLabel));
      }

      // on Qualcomm we only emit Dref instructions against 2D textures, otherwise the compiler may
      // crash.
      if(m_pDriver->GetDriverInfo().QualcommDrefNon2DCompileCrash())
        depthTex &= (i == (uint32_t)ShaderDebugBind::Tex2D);

      // VUID-StandaloneSpirv-OpImage-04777
      // OpImage*Dref must not consume an image whose Dim is 3D
      if(i == (uint32_t)ShaderDebugBind::Tex3D)
        depthTex = false;

      // don't emit dref's for uint/sint textures
      if(uintTex || sintTex)
        depthTex = false;

      if(depthTex)
      {
        for(rdcspv::Op op :
            {rdcspv::Op::ImageSampleDrefExplicitLod, rdcspv::Op::ImageSampleDrefImplicitLod})
        {
          rdcspv::Id label = editor.MakeId();
          targets.push_back({(uint32_t)op * 10 + i, label});

          cases.add(rdcspv::OpLabel(label));
          rdcspv::Id loadedImage =
              cases.add(rdcspv::OpLoad(texSampTypes[i], editor.MakeId(), bindVars[i]));
          rdcspv::Id loadedSampler =
              cases.add(rdcspv::OpLoad(texSampTypes[sampIdx], editor.MakeId(), bindVars[sampIdx]));

          rdcspv::Id mergeLabel = editor.MakeId();
          rdcspv::Id gradCase = editor.MakeId();
          rdcspv::Id lodCase = editor.MakeId();
          cases.add(rdcspv::OpSelectionMerge(mergeLabel, rdcspv::SelectionControl::None));
          cases.add(rdcspv::OpBranchConditional(useGradOrGatherOffsets, gradCase, lodCase));

          rdcspv::Id lodResult;
          {
            cases.add(rdcspv::OpLabel(lodCase));
            rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;
            operands.setLod(lod);
            rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
                texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

            lodResult = cases.add(rdcspv::OpImageSampleDrefExplicitLod(
                scalarResultType, editor.MakeId(), combined, coord[i], compare, operands));

            cases.add(rdcspv::OpBranch(mergeLabel));
          }

          rdcspv::Id gradResult;
          {
            cases.add(rdcspv::OpLabel(gradCase));
            rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;
            operands.setGrad(ddxs[i], ddys[i]);
            if(m_pDriver->GetDeviceEnabledFeatures().shaderResourceMinLod)
              operands.setMinLod(minlod);
            rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
                texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

            gradResult = cases.add(rdcspv::OpImageSampleDrefExplicitLod(
                scalarResultType, editor.MakeId(), combined, coord[i], compare, operands));

            cases.add(rdcspv::OpBranch(mergeLabel));
          }

          cases.add(rdcspv::OpLabel(mergeLabel));
          rdcspv::Id scalarSampleResult = cases.add(rdcspv::OpPhi(
              scalarResultType, editor.MakeId(), {{lodResult, lodCase}, {gradResult, gradCase}}));
          rdcspv::Id sampleResult = cases.add(rdcspv::OpCompositeConstruct(
              resultType, editor.MakeId(), {scalarSampleResult, zerof, zerof, zerof}));
          cases.add(rdcspv::OpStore(outVar, sampleResult));
          cases.add(rdcspv::OpBranch(breakLabel));
        }
      }

      // can only gather with 2D/Cube textures
      if(i == (uint32_t)ShaderDebugBind::Tex1D || i == (uint32_t)ShaderDebugBind::Tex3D)
        continue;

      for(rdcspv::Op op : {rdcspv::Op::ImageGather, rdcspv::Op::ImageDrefGather})
      {
        if(op == rdcspv::Op::ImageDrefGather && !depthTex)
          continue;

        rdcspv::Id label = editor.MakeId();
        targets.push_back({(uint32_t)op * 10 + i, label});

        cases.add(rdcspv::OpLabel(label));
        rdcspv::Id loadedImage =
            cases.add(rdcspv::OpLoad(texSampTypes[i], editor.MakeId(), bindVars[i]));
        rdcspv::Id loadedSampler =
            cases.add(rdcspv::OpLoad(texSampTypes[sampIdx], editor.MakeId(), bindVars[sampIdx]));

        rdcspv::Id sampleResult;
        if(m_pDriver->GetDeviceEnabledFeatures().shaderImageGatherExtended)
        {
          rdcspv::Id mergeLabel = editor.MakeId();
          rdcspv::Id constsCase = editor.MakeId();
          rdcspv::Id baseCase = editor.MakeId();
          cases.add(rdcspv::OpSelectionMerge(mergeLabel, rdcspv::SelectionControl::None));
          cases.add(rdcspv::OpBranchConditional(useGradOrGatherOffsets, constsCase, baseCase));

          rdcspv::Id baseResult;
          {
            cases.add(rdcspv::OpLabel(baseCase));

            rdcspv::ImageOperandsAndParamDatas operands;

            if(dynoffset[i] != rdcspv::Id())
              operands.setOffset(dynoffset[i]);

            rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
                texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

            if(op == rdcspv::Op::ImageGather)
              baseResult = cases.add(rdcspv::OpImageGather(resultType, editor.MakeId(), combined,
                                                           coord[i], gatherChannel, operands));
            else
              baseResult = cases.add(rdcspv::OpImageDrefGather(
                  resultType, editor.MakeId(), combined, coord[i], compare, operands));

            cases.add(rdcspv::OpBranch(mergeLabel));
          }

          rdcspv::Id constsResult;
          {
            cases.add(rdcspv::OpLabel(constsCase));
            rdcspv::ImageOperandsAndParamDatas operands;    // don't use the offsets above

            // if this feature isn't available, this path will never be exercised (since we only
            // come in here when the actual shader used const offsets) so it's fine to drop it in
            // that case to ensure the module is still legal.
            if(m_pDriver->GetDeviceEnabledFeatures().shaderImageGatherExtended &&
               i != (uint32_t)ShaderDebugBind::TexCube)
              operands.setConstOffsets(gatherOffsets);

            rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
                texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

            if(op == rdcspv::Op::ImageGather)
              constsResult = cases.add(rdcspv::OpImageGather(resultType, editor.MakeId(), combined,
                                                             coord[i], gatherChannel, operands));
            else
              constsResult = cases.add(rdcspv::OpImageDrefGather(
                  resultType, editor.MakeId(), combined, coord[i], compare, operands));

            cases.add(rdcspv::OpBranch(mergeLabel));
          }

          cases.add(rdcspv::OpLabel(mergeLabel));
          sampleResult = cases.add(rdcspv::OpPhi(
              resultType, editor.MakeId(), {{baseResult, baseCase}, {constsResult, constsCase}}));
        }
        else
        {
          rdcspv::ImageOperandsAndParamDatas operands = imageOperandsWithOffsets;

          rdcspv::Id combined = cases.add(rdcspv::OpSampledImage(
              texSampCombinedTypes[i], editor.MakeId(), loadedImage, loadedSampler));

          if(op == rdcspv::Op::ImageGather)
            sampleResult = cases.add(rdcspv::OpImageGather(resultType, editor.MakeId(), combined,
                                                           coord[i], gatherChannel, operands));
          else
            sampleResult = cases.add(rdcspv::OpImageDrefGather(
                resultType, editor.MakeId(), combined, coord[i], compare, operands));
        }

        cases.add(rdcspv::OpStore(outVar, sampleResult));
        cases.add(rdcspv::OpBranch(breakLabel));
      }
    }

    func.add(rdcspv::OpSelectionMerge(breakLabel, rdcspv::SelectionControl::None));
    func.add(rdcspv::OpSwitch32(switchVal, defaultLabel, targets));

    func.append(cases);

    // default: store NULL data
    func.add(rdcspv::OpLabel(defaultLabel));
    func.add(rdcspv::OpStore(
        outVar, editor.AddConstant(rdcspv::OpConstantNull(resultType, editor.MakeId()))));
    func.add(rdcspv::OpBranch(breakLabel));

    func.add(rdcspv::OpLabel(breakLabel));
    func.add(rdcspv::OpReturn());
    func.add(rdcspv::OpFunctionEnd());

    editor.AddFunction(func);
  }

  const uint64_t deviceThreadID;
};

enum class InputSpecConstant
{
  Address = 0,
  AddressMSB,
  ArrayLength,
  DestX,
  DestY,
  DestThreadIDX,
  DestThreadIDY,
  DestThreadIDZ,
  DestInstance,
  DestVertex,
  DestView,
  Count,
};

struct SpecData
{
  VkDeviceAddress bufferAddress;
  uint32_t arrayLength;
  uint32_t destVertex;
  uint32_t destInstance;
  uint32_t destView;
  float destX;
  float destY;
  uint32_t globalThreadIdX;
  uint32_t globalThreadIdY;
  uint32_t globalThreadIdZ;
};

static const VkSpecializationMapEntry specMapsTemplate[] = {
    {
        (uint32_t)InputSpecConstant::Address,
        offsetof(SpecData, bufferAddress),
        // EXT_bda uses a 64-bit constant, as well as KHR_bda64
        0,
    },
    {
        (uint32_t)InputSpecConstant::ArrayLength,
        offsetof(SpecData, arrayLength),
        sizeof(SpecData::arrayLength),
    },
    {
        (uint32_t)InputSpecConstant::DestX,
        offsetof(SpecData, destX),
        sizeof(SpecData::destX),
    },
    {
        (uint32_t)InputSpecConstant::DestY,
        offsetof(SpecData, destY),
        sizeof(SpecData::destY),
    },
    {
        (uint32_t)InputSpecConstant::DestThreadIDX,
        offsetof(SpecData, globalThreadIdX),
        sizeof(SpecData::globalThreadIdX),
    },
    {
        (uint32_t)InputSpecConstant::DestThreadIDY,
        offsetof(SpecData, globalThreadIdY),
        sizeof(SpecData::globalThreadIdY),
    },
    {
        (uint32_t)InputSpecConstant::DestThreadIDZ,
        offsetof(SpecData, globalThreadIdZ),
        sizeof(SpecData::globalThreadIdZ),
    },
    {
        (uint32_t)InputSpecConstant::DestInstance,
        offsetof(SpecData, destInstance),
        sizeof(SpecData::destInstance),
    },
    {
        (uint32_t)InputSpecConstant::DestVertex,
        offsetof(SpecData, destVertex),
        sizeof(SpecData::destVertex),
    },
    {
        (uint32_t)InputSpecConstant::DestView,
        offsetof(SpecData, destView),
        sizeof(SpecData::destView),
    },
    {
        (uint32_t)InputSpecConstant::AddressMSB,
        offsetof(SpecData, bufferAddress) + 4,
        sizeof(uint32_t),
    },
};

RDCCOMPILE_ASSERT((size_t)InputSpecConstant::Count == ARRAY_COUNT(specMapsTemplate),
                  "Spec constants changed");

enum class SubgroupCapability : uint32_t
{
  None = 0,
  EXTBallot,
  Vulkan1_1_NoBallot,
  Vulkan1_1,
};

static const uint32_t validMagicNumber = 12345;
static const uint32_t NumReservedBindings = 1;

// we use the message passing method from the quadoverdraw to swap data between quad neighbours
// using fine derivatives. This is based on "Shader Amortization using Pixel Quad Message Passing",
// Eric Penner, GPU Pro 2.
//
// broadly, if we take ddx_fine and either add or subtract it we can swap horizontal information,
// and similarly vertical for ddy_fine. To swap across the diagonal we perform one fine derivative
// swap, and then use the other fine derivative on the result of that swap.
//
// +---+---+
// | 0 | 1 |
// +---+---+
// | 2 | 3 |
// +---+---+
//
// fine derivatives are obtained by subtracting the right-most neighbour from left-most in each row,
// and the bottom-most neighbour from top-most in each column.
//
// the pseudocode is as follows (following the quad overdraw closely) where X is the type of our value:
//
//
// bool quadX = (quadLaneIndex & 1) != 0;
// bool quadY = (quadLaneIndex & 2) != 0;
//
// bool readX = (readIndex & 1) != 0;
// bool readY = (readIndex & 2) != 0;
//
// float sign_x = quadX ? -1 : 1;
// float sign_y = quadY ? -1 : 1;
//
// X c1 = c0 + sign_x * ddx_fine(c0);
// X c2 = c0 + sign_y * ddy_fine(c0);
// X c3 = c2 + sign_x * ddx_fine(c2);
//
// if(readIndex == quadLaneIndex) // identity, handle gracefully
//   return c0;
// else if(readY == quadY) // horizontal neighbour
//   return c1;
// else if(readX == quadX) // vertical neighbour
//	return c2;
// else
//   return c3; // diagonal neighbour
//

static rdcspv::Id AddQuadSwizzleHelper(rdcspv::Editor &editor, uint32_t count)
{
  rdcspv::Id func = editor.MakeId();

  rdcspv::OperationList ops;

  rdcspv::Id u32 = editor.DeclareType(rdcspv::scalar<uint32_t>());

  rdcspv::Id type;
  if(count == 1)
    type = editor.DeclareType(rdcspv::scalar<float>());
  else
    type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), count));

  rdcspv::Id funcType = editor.DeclareType(rdcspv::FunctionType(type, {type, u32, u32}));

  ops.add(rdcspv::OpFunction(type, func, rdcspv::FunctionControl::None, funcType));
  rdcspv::Id c0 = ops.add(rdcspv::OpFunctionParameter(type, editor.MakeId()));
  rdcspv::Id quadLaneIndex = ops.add(rdcspv::OpFunctionParameter(u32, editor.MakeId()));
  rdcspv::Id readIndex = ops.add(rdcspv::OpFunctionParameter(u32, editor.MakeId()));
  ops.add(rdcspv::OpLabel(editor.MakeId()));

  editor.SetName(c0, "c0");
  editor.SetName(quadLaneIndex, "quadLaneIndex");
  editor.SetName(readIndex, "readIndex");

  rdcspv::Id zero = editor.AddConstantImmediate<uint32_t>(0U);
  rdcspv::Id one = editor.AddConstantImmediate<uint32_t>(1U);
  rdcspv::Id two = editor.AddConstantImmediate<uint32_t>(2U);

  rdcspv::Id posOne = editor.AddConstantImmediate<float>(1.0f);
  rdcspv::Id negOne = editor.AddConstantImmediate<float>(-1.0f);

  rdcspv::Id boolType = editor.DeclareType(rdcspv::scalar<bool>());

  rdcspv::Id quadX = ops.add(rdcspv::OpBitwiseAnd(u32, editor.MakeId(), quadLaneIndex, one));
  quadX = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), quadX, zero));
  rdcspv::Id quadY = ops.add(rdcspv::OpBitwiseAnd(u32, editor.MakeId(), quadLaneIndex, two));
  quadY = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), quadY, zero));

  rdcspv::Id readX = ops.add(rdcspv::OpBitwiseAnd(u32, editor.MakeId(), readIndex, one));
  readX = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), readX, zero));
  rdcspv::Id readY = ops.add(rdcspv::OpBitwiseAnd(u32, editor.MakeId(), readIndex, two));
  readY = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), readY, zero));

  rdcspv::Id horizNeighbour =
      ops.add(rdcspv::OpLogicalEqual(boolType, editor.MakeId(), readY, quadY));
  rdcspv::Id vertNeighbour = ops.add(rdcspv::OpLogicalEqual(boolType, editor.MakeId(), readX, quadX));
  rdcspv::Id isIdentity =
      ops.add(rdcspv::OpIEqual(boolType, editor.MakeId(), quadLaneIndex, readIndex));

  editor.SetName(quadX, "quadX");
  editor.SetName(quadY, "quadY");
  editor.SetName(readX, "readX");
  editor.SetName(readY, "readY");

  if(count >= 2)
  {
    rdcspv::Id floatNtype = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), count));
    rdcspv::Id boolNtype = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<bool>(), count));

    rdcarray<rdcspv::Id> bcast;

    bcast.fill(count, posOne);
    posOne = ops.add(rdcspv::OpCompositeConstruct(floatNtype, editor.MakeId(), bcast));
    bcast.fill(count, negOne);
    negOne = ops.add(rdcspv::OpCompositeConstruct(floatNtype, editor.MakeId(), bcast));

    bcast.fill(count, quadX);
    quadX = ops.add(rdcspv::OpCompositeConstruct(boolNtype, editor.MakeId(), bcast));
    bcast.fill(count, quadY);
    quadY = ops.add(rdcspv::OpCompositeConstruct(boolNtype, editor.MakeId(), bcast));
  }

  rdcspv::Id sign_x = ops.add(rdcspv::OpSelect(type, editor.MakeId(), quadX, negOne, posOne));
  rdcspv::Id sign_y = ops.add(rdcspv::OpSelect(type, editor.MakeId(), quadY, negOne, posOne));
  editor.SetName(sign_x, "sign_x");
  editor.SetName(sign_y, "sign_y");

  rdcspv::Id ddxFine = ops.add(rdcspv::OpDPdxFine(type, editor.MakeId(), c0));
  rdcspv::Id ddyFine = ops.add(rdcspv::OpDPdyFine(type, editor.MakeId(), c0));

  rdcspv::Id c1 = ops.add(rdcspv::OpFMul(type, editor.MakeId(), sign_x, ddxFine));
  c1 = ops.add(rdcspv::OpFAdd(type, editor.MakeId(), c0, c1));
  editor.SetName(c1, "c1");

  rdcspv::Id c2 = ops.add(rdcspv::OpFMul(type, editor.MakeId(), sign_y, ddyFine));
  c2 = ops.add(rdcspv::OpFAdd(type, editor.MakeId(), c0, c2));
  editor.SetName(c2, "c2");

  rdcspv::Id ddxC2 = ops.add(rdcspv::OpDPdxFine(type, editor.MakeId(), c2));

  rdcspv::Id c3 = ops.add(rdcspv::OpFMul(type, editor.MakeId(), sign_x, ddxC2));
  c3 = ops.add(rdcspv::OpFAdd(type, editor.MakeId(), c2, c3));
  editor.SetName(c3, "c3");

  rdcspv::Id trueBranch = editor.MakeId(), falseBranch = editor.MakeId(),
             mergeBranch = editor.MakeId();

  // we'll want to read lane 0 on all lanes for the quadId, so handle that specially
  ops.add(rdcspv::OpSelectionMerge(mergeBranch, rdcspv::SelectionControl::None));
  ops.add(rdcspv::OpBranchConditional(isIdentity, trueBranch, falseBranch));

  ops.add(rdcspv::OpLabel(trueBranch));
  ops.add(rdcspv::OpReturnValue(c0));
  ops.add(rdcspv::OpLabel(falseBranch));
  ops.add(rdcspv::OpBranch(mergeBranch));
  ops.add(rdcspv::OpLabel(mergeBranch));

  // expanded flow control. Impossible labels happen after returns on both branches
  //
  // if(isIdentity) {
  //   ident;
  // }
  // notident;
  //
  // if(horizNeighbour) {
  //   horiz;
  // } else {
  //   notHoriz;
  //   if(vertNeighbour) { vert; } else { diagonal; }
  //   impossible1;
  // }
  // impossible2;
  rdcspv::Id horiz = editor.MakeId(), notHoriz = editor.MakeId(), vert = editor.MakeId(),
             diagonal = editor.MakeId(), imposs1 = editor.MakeId(), imposs2 = editor.MakeId();

  ops.add(rdcspv::OpSelectionMerge(imposs2, rdcspv::SelectionControl::None));
  ops.add(rdcspv::OpBranchConditional(horizNeighbour, horiz, notHoriz));

  ops.add(rdcspv::OpLabel(horiz));
  ops.add(rdcspv::OpReturnValue(c1));
  ops.add(rdcspv::OpLabel(notHoriz));

  ops.add(rdcspv::OpSelectionMerge(imposs1, rdcspv::SelectionControl::None));
  ops.add(rdcspv::OpBranchConditional(vertNeighbour, vert, diagonal));

  ops.add(rdcspv::OpLabel(vert));
  ops.add(rdcspv::OpReturnValue(c2));
  ops.add(rdcspv::OpLabel(diagonal));
  ops.add(rdcspv::OpReturnValue(c3));

  ops.add(rdcspv::OpLabel(imposs1));
  ops.add(rdcspv::OpUnreachable());
  ops.add(rdcspv::OpLabel(imposs2));
  ops.add(rdcspv::OpUnreachable());

  ops.add(rdcspv::OpFunctionEnd());

  editor.AddFunction(ops);
  editor.SetName(func, "quadSwizzleHelper");

  return func;
}

static void CreateInputFetcher(rdcarray<uint32_t> &spv, const rdcarray<SpecConstant> &userSpec,
                               VulkanCreationInfo::ShaderModuleReflection &shadRefl,
                               BufferStorageMode storageMode, bool usePrimitiveID, bool useSampleID,
                               bool useViewIndex, SubgroupCapability subgroupCapability,
                               uint32_t maxSubgroupSize)
{
  rdcspv::Editor editor(spv);

  ShaderStage stage = ShaderStage(shadRefl.stageIndex);
  rdcspv::ThreadScope threadScope = shadRefl.patchData.threadScope;

  uint32_t paramAlign = 16;

  for(const SigParameter &sig : shadRefl.refl->inputSignature)
  {
    if(VarTypeByteSize(sig.varType) * sig.compCount > paramAlign)
      paramAlign = 32;
  }

  // conservatively calculate structure stride with full amount for every input element
  uint32_t structStride = (uint32_t)shadRefl.refl->inputSignature.size() * paramAlign;

  switch(stage)
  {
    case ShaderStage::Vertex: structStride += sizeof(rdcspv::VertexLaneData); break;
    case ShaderStage::Pixel: structStride += sizeof(rdcspv::PixelLaneData); break;
    case ShaderStage::Task:
    case ShaderStage::Mesh:
    case ShaderStage::Compute: structStride += sizeof(rdcspv::ComputeLaneData); break;
    default: break;
  }

  if(threadScope & rdcspv::ThreadScope::Subgroup)
  {
    structStride += sizeof(rdcspv::SubgroupLaneData);
  }

  // simulating full subgroups with ballot ability to read other lanes, we read all lanes data
  const bool fullSubgroups = (subgroupCapability == SubgroupCapability::EXTBallot ||
                              subgroupCapability == SubgroupCapability::Vulkan1_1) &&
                             (threadScope & rdcspv::ThreadScope::Subgroup);
  // faking subgroups without reading a subgroup's worth of data but we still read lane index and elect value
  const bool minimalSubgroups = subgroupCapability != SubgroupCapability::None &&
                                (threadScope & rdcspv::ThreadScope::Subgroup);

  editor.Prepare();
  editor.SetBufferStorageMode(storageMode);

  editor.FlattenSpecConstants(userSpec);

  // remove any OpSource
  {
    // remove any OpName that refers to deleted IDs - functions or results
    rdcspv::Iter it = editor.Begin(rdcspv::Section::DebugStringSource);
    rdcspv::Iter end = editor.End(rdcspv::Section::DebugStringSource);
    while(it < end)
    {
      if(it.opcode() == rdcspv::Op::Source || it.opcode() == rdcspv::Op::SourceContinued)
      {
        editor.Remove(it);
      }
      it++;
    }
  }

  editor.OffsetBindingsToMatchReservation(NumReservedBindings);

  // the original entry ID we're patching
  rdcspv::Id originalEntry = editor.FindEntryID({shadRefl.entryPoint, stage});
  // the new wrapped entry function we'll add
  rdcspv::Id entryID = editor.MakeId();

  // repoint the entry declaration
  editor.ChangeEntry(originalEntry, entryID);

  rdcspv::MemoryAccessAndParamDatas alignedAccess;
  alignedAccess.setAligned(sizeof(uint32_t));

  rdcspv::Id uint32Type = editor.DeclareType(rdcspv::scalar<uint32_t>());
  rdcspv::Id floatType = editor.DeclareType(rdcspv::scalar<float>());
  rdcspv::Id boolType = editor.DeclareType(rdcspv::scalar<bool>());
  rdcspv::Id uint3Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<uint32_t>(), 3));
  rdcspv::Id uint4Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<uint32_t>(), 4));
  rdcspv::Id float4Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 4));
  rdcspv::Id float3Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 3));
  rdcspv::Id float2Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<float>(), 2));
  rdcspv::Id bool4Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<bool>(), 4));

  rdcarray<rdcspv::Id> newGlobals;

  rdcspv::Id LaneDataStruct;

  enum ResultBaseMember
  {
    ResultBase_pos,
    ResultBase_prim,
    ResultBase_sample,
    ResultBase_view,
    ResultBase_valid,
    ResultBase_ddxDerivCheck,
    ResultBase_quadLaneIndex,
    ResultBase_laneIndex,
    ResultBase_subgroupSize,
    ResultBase_globalBallot,
    ResultBase_electBallot,
    ResultBase_helperBallot,
    ResultBase_numSubgroups,
    ResultBase_firstUser,
  };

  struct laneValue
  {
    rdcstr name;
    // index in the LaneData struct
    size_t structIndex;
    // type ID of the value (float4, uint, etc)
    rdcspv::Id type;
    // direct loaded value per-lane
    rdcspv::Id base;

    // for pixel shaders, the quad's worth of swizzled data to load helper info from, whether or not
    // we have subgroups active
    rdcarray<rdcspv::Id> quadSwizzledData;

    // the loadOps to prepare the base value, done separately so we can push each fixed value into
    // an array while creating the struct to all be processed agnostically, instead of separating adding
    // them to the struct from loading them OR interleaving struct definition while preparing our function
    rdcspv::OperationList loadOps;

    bool flat = false;
  };

  rdcarray<laneValue> laneValues;

  rdcspv::Id subgroupScope = editor.AddConstantImmediate<uint32_t>((uint32_t)rdcspv::Scope::Subgroup);

  rdcspv::Id isHelper, quadLaneIndex, quadId;

  {
    rdcarray<rdcspv::StructMember> structMembers;
    uint32_t offset = 0;

    // declare fixed lane data first

    if(threadScope & rdcspv::ThreadScope::Subgroup)
    {
      laneValue elect;
      elect.name = "__rd_globalElect";
      elect.structIndex = structMembers.size();
      elect.type = uint32Type;
      // don't store elect value for legacy KHR path
      if(subgroupCapability == SubgroupCapability::Vulkan1_1 ||
         subgroupCapability == SubgroupCapability::Vulkan1_1_NoBallot)
      {
        elect.base = elect.loadOps.add(
            rdcspv::OpGroupNonUniformElect(boolType, editor.MakeId(), subgroupScope));
        elect.base = elect.loadOps.add(rdcspv::OpSelect(uint32Type, editor.MakeId(), elect.base,
                                                        editor.AddConstantImmediate<uint32_t>(1),
                                                        editor.AddConstantImmediate<uint32_t>(0)));
        editor.SetName(elect.base, elect.name);
      }
      else
      {
        elect.base = editor.AddConstantImmediate<uint32_t>(0);
        elect.flat = true;
      }
      laneValues.push_back(elect);
      structMembers.push_back(
          {uint32Type, elect.name, offset + (uint32_t)offsetof(rdcspv::SubgroupLaneData, elect)});

      // we implicitly only write data for active lanes so we just set isActive to 1 always
      laneValue isActive;
      isActive.name = "__rd_active";
      isActive.structIndex = structMembers.size();
      isActive.type = uint32Type;
      isActive.base = editor.AddConstantImmediate<uint32_t>(1);
      isActive.flat = true;
      laneValues.push_back(isActive);
      structMembers.push_back({uint32Type, isActive.name,
                               offset + (uint32_t)offsetof(rdcspv::SubgroupLaneData, isActive)});

      structMembers.push_back(
          {uint32Type, "__pad", offset + (uint32_t)offsetof(rdcspv::SubgroupLaneData, padding)});
      structMembers.push_back(
          {uint32Type, "__pad",
           uint32_t(offset + offsetof(rdcspv::SubgroupLaneData, padding) + sizeof(uint32_t))});

      offset += sizeof(rdcspv::SubgroupLaneData);
      RDCCOMPILE_ASSERT((sizeof(rdcspv::SubgroupLaneData) / sizeof(Vec4f)) * sizeof(Vec4f) ==
                            sizeof(rdcspv::SubgroupLaneData),
                        "SubgroupLaneData is misaligned, ensure 16-byte aligned");
    }

    if(stage == ShaderStage::Vertex)
    {
      laneValue inst;
      inst.name = "__rd_inst";
      inst.structIndex = structMembers.size();
      inst.type = uint32Type;
      inst.base = editor.AddBuiltinInputLoad(inst.loadOps, newGlobals, stage,
                                             rdcspv::BuiltIn::InstanceIndex, uint32Type);
      editor.SetName(inst.base, inst.name);
      laneValues.push_back(inst);
      structMembers.push_back(
          {uint32Type, inst.name, offset + (uint32_t)offsetof(rdcspv::VertexLaneData, inst)});

      laneValue vert;
      vert.name = "__rd_vert";
      vert.structIndex = structMembers.size();
      vert.type = uint32Type;
      vert.base = editor.AddBuiltinInputLoad(vert.loadOps, newGlobals, stage,
                                             rdcspv::BuiltIn::VertexIndex, uint32Type);
      editor.SetName(vert.base, vert.name);
      laneValues.push_back(vert);
      structMembers.push_back(
          {uint32Type, vert.name, offset + (uint32_t)offsetof(rdcspv::VertexLaneData, vert)});

      if(useViewIndex)
      {
        laneValue view;
        view.name = "__rd_view";
        view.structIndex = structMembers.size();
        view.type = uint32Type;
        view.base = editor.AddBuiltinInputLoad(view.loadOps, newGlobals, stage,
                                               rdcspv::BuiltIn::ViewIndex, uint32Type);
        editor.SetName(view.base, view.name);
        laneValues.push_back(view);
        structMembers.push_back(
            {uint32Type, view.name, offset + (uint32_t)offsetof(rdcspv::VertexLaneData, view)});

        structMembers.push_back(
            {uint32Type, "__pad", offset + (uint32_t)offsetof(rdcspv::VertexLaneData, padding)});
      }
      else
      {
        structMembers.push_back(
            {uint32Type, "__rd_view", offset + (uint32_t)offsetof(rdcspv::VertexLaneData, view)});
        structMembers.push_back(
            {uint32Type, "__pad", offset + (uint32_t)offsetof(rdcspv::VertexLaneData, padding)});
      }

      offset += sizeof(rdcspv::VertexLaneData);
      RDCCOMPILE_ASSERT((sizeof(rdcspv::VertexLaneData) / sizeof(Vec4f)) * sizeof(Vec4f) ==
                            sizeof(rdcspv::VertexLaneData),
                        "VertexLaneData is misaligned, ensure 16-byte aligned");
    }
    else if(stage == ShaderStage::Pixel)
    {
      laneValue fragCoord;
      fragCoord.name = "__rd_pixelPos";
      fragCoord.structIndex = structMembers.size();
      fragCoord.type = float4Type;
      fragCoord.base = editor.AddBuiltinInputLoad(fragCoord.loadOps, newGlobals, stage,
                                                  rdcspv::BuiltIn::FragCoord, float4Type);
      editor.SetName(fragCoord.base, fragCoord.name);
      laneValues.push_back(fragCoord);
      structMembers.push_back({float4Type, fragCoord.name,
                               offset + (uint32_t)offsetof(rdcspv::PixelLaneData, fragCoord)});

      laneValue helper;
      helper.name = "__rd_isHelper";
      helper.structIndex = structMembers.size();
      helper.type = uint32Type;
      helper.base = editor.AddBuiltinInputLoad(helper.loadOps, newGlobals, stage,
                                               rdcspv::BuiltIn::HelperInvocation, boolType);
      helper.base = helper.loadOps.add(rdcspv::OpSelect(uint32Type, editor.MakeId(), helper.base,
                                                        editor.AddConstantImmediate<uint32_t>(1),
                                                        editor.AddConstantImmediate<uint32_t>(0)));
      editor.SetName(helper.base, helper.name);
      laneValues.push_back(helper);
      structMembers.push_back(
          {uint32Type, helper.name, offset + (uint32_t)offsetof(rdcspv::PixelLaneData, isHelper)});

      laneValue quad;
      quad.name = "__rd_quadId";
      quad.structIndex = structMembers.size();
      quad.type = uint32Type;
      quad.flat = true;
      // this will be handled specially similarly to helper
      quad.base = editor.MakeId();
      editor.SetName(quad.base, quad.name);
      laneValues.push_back(quad);
      structMembers.push_back(
          {uint32Type, quad.name, offset + (uint32_t)offsetof(rdcspv::PixelLaneData, quadId)});

      laneValue quadLane;
      quadLane.name = "__rd_quadLane";
      quadLane.structIndex = structMembers.size();
      quadLane.type = uint32Type;
      quadLane.base = editor.MakeId();
      editor.SetName(quadLane.base, quadLane.name);
      laneValues.push_back(quadLane);
      structMembers.push_back({uint32Type, quadLane.name,
                               offset + (uint32_t)offsetof(rdcspv::PixelLaneData, quadLaneIndex)});

      // quad properties will be handled specially
      isHelper = helper.base;
      quadId = quad.base;
      quadLaneIndex = quadLane.base;

      structMembers.push_back(
          {uint32Type, "__pad", offset + (uint32_t)offsetof(rdcspv::PixelLaneData, padding)});

      offset += sizeof(rdcspv::PixelLaneData);
      RDCCOMPILE_ASSERT((sizeof(rdcspv::PixelLaneData) / sizeof(Vec4f)) * sizeof(Vec4f) ==
                            sizeof(rdcspv::PixelLaneData),
                        "PixelLaneData is misaligned, ensure 16-byte aligned");
    }
    else if(stage == ShaderStage::Compute || stage == ShaderStage::Task || stage == ShaderStage::Mesh)
    {
      laneValue threadid;
      threadid.name = "__rd_threadid";
      threadid.structIndex = structMembers.size();
      threadid.type = uint3Type;
      threadid.base = editor.AddBuiltinInputLoad(threadid.loadOps, newGlobals, stage,
                                                 rdcspv::BuiltIn::LocalInvocationId, uint3Type);
      editor.SetName(threadid.base, threadid.name);
      laneValues.push_back(threadid);
      structMembers.push_back({uint3Type, threadid.name,
                               offset + (uint32_t)offsetof(rdcspv::ComputeLaneData, threadid)});

      laneValue subid;
      subid.name = "__rd_subgroupid";
      subid.structIndex = structMembers.size();
      subid.type = uint32Type;
      subid.base = editor.AddBuiltinInputLoad(subid.loadOps, newGlobals, stage,
                                              rdcspv::BuiltIn::SubgroupId, uint32Type);
      editor.SetName(subid.base, subid.name);
      laneValues.push_back(subid);
      structMembers.push_back({uint32Type, subid.name,
                               offset + (uint32_t)offsetof(rdcspv::ComputeLaneData, subIdxInGroup)});

      offset += sizeof(rdcspv::ComputeLaneData);
      RDCCOMPILE_ASSERT((sizeof(rdcspv::ComputeLaneData) / sizeof(Vec4f)) * sizeof(Vec4f) ==
                            sizeof(rdcspv::ComputeLaneData),
                        "ComputeLaneData is misaligned, ensure 16-byte aligned");
    }

    // now add input signature values

    for(size_t i = 0; i < shadRefl.refl->inputSignature.size(); i++)
    {
      const SPIRVInterfaceAccess &access = shadRefl.patchData.inputs[i];
      const SigParameter &param = shadRefl.refl->inputSignature[i];

      rdcspv::Scalar base = rdcspv::scalar(param.varType);

      uint32_t width = (base.width / 8);

      rdcspv::Id loadType;

      if(param.compCount == 1)
        loadType = editor.DeclareType(base);
      else
        loadType = editor.DeclareType(rdcspv::Vector(base, param.compCount));

      rdcspv::Id valueType;

      // treat bools as uints
      if(base.type == rdcspv::Op::TypeBool)
        width = 4;

      // we immediately upconvert any sub-32-bit types
      if(width < 4)
      {
        width = 4;
        base.width = 32;

        if(param.compCount == 1)
          valueType = editor.DeclareType(base);
        else
          valueType = editor.DeclareType(rdcspv::Vector(base, param.compCount));
      }
      else
      {
        valueType = loadType;
      }

      rdcarray<rdcspv::Id> accessIndices;
      for(uint32_t idx : access.accessChain)
        accessIndices.push_back(editor.AddConstantImmediate<uint32_t>(idx));

      rdcspv::Id inputPtrType =
          editor.DeclareType(rdcspv::Pointer(loadType, rdcspv::StorageClass::Input));

      laneValue value;
      value.name = param.varName;
      value.structIndex = structMembers.size();
      value.type = valueType;

      if(value.name.beginsWith("gl_"))
        value.name = "__rd_" + value.name.substr(3);

      // if we have no access chain it's a global pointer of the type we want, so just load
      // straight out of it
      rdcspv::Id ptr;
      if(accessIndices.empty())
        ptr = access.ID;
      else
        ptr = value.loadOps.add(
            rdcspv::OpAccessChain(inputPtrType, editor.MakeId(), access.ID, accessIndices));

      value.base = value.loadOps.add(rdcspv::OpLoad(loadType, editor.MakeId(), ptr));
      if(valueType == boolType)
      {
        valueType = uint32Type;
        // can't store bools directly, need to convert to uint
        value.base = value.loadOps.add(rdcspv::OpSelect(valueType, editor.MakeId(), value.base,
                                                        editor.AddConstantImmediate<uint32_t>(1),
                                                        editor.AddConstantImmediate<uint32_t>(0)));
      }
      if(valueType != loadType)
      {
        if(VarTypeCompType(param.varType) == CompType::Float)
          value.base = value.loadOps.add(rdcspv::OpFConvert(valueType, editor.MakeId(), value.base));
        else if(VarTypeCompType(param.varType) == CompType::SInt)
          value.base = value.loadOps.add(rdcspv::OpSConvert(valueType, editor.MakeId(), value.base));
        else if(VarTypeCompType(param.varType) == CompType::UInt)
          value.base = value.loadOps.add(rdcspv::OpUConvert(valueType, editor.MakeId(), value.base));
      }
      editor.SetName(value.base, StringFormat::Fmt("__rd_base_%zu_%s", i, param.varName.c_str()));
      // non-float inputs are considered flat
      value.flat = VarTypeCompType(param.varType) != CompType::Float;

      // mark this as non-flat so we still derive it for helper lanes as it will vary
      if(param.systemValue == ShaderBuiltin::IndexInSubgroup)
        value.flat = false;

      laneValues.push_back(value);

      if(valueType == boolType)
        structMembers.push_back({uint32Type, value.name, offset});
      else
        structMembers.push_back({valueType, value.name, offset});
      offset += param.compCount * width;

      // align offset conservatively, to 16-byte aligned. We do this with explicit uints so we can
      // preview with spirv-cross (and because it doesn't cost anything particularly)
      uint32_t paddingWords = ((paramAlign - (offset % 16)) / 4) % 4;
      for(uint32_t p = 0; p < paddingWords; p++)
      {
        structMembers.push_back({uint32Type, "__pad", offset});
        offset += 4;
      }
    }

    RDCASSERT(offset <= structStride);

    LaneDataStruct = editor.DeclareStructType("__rd_LaneData", structMembers);
  }

  rdcspv::Id arrayLength =
      editor.AddSpecConstantImmediate<uint32_t>(1U, (uint32_t)InputSpecConstant::ArrayLength);

  editor.SetName(arrayLength, "arrayLength");

  rdcspv::Id destX = editor.AddSpecConstantImmediate<float>(0.0f, (uint32_t)InputSpecConstant::DestX);
  rdcspv::Id destY = editor.AddSpecConstantImmediate<float>(0.0f, (uint32_t)InputSpecConstant::DestY);

  editor.SetName(destX, "destX");
  editor.SetName(destY, "destY");

  rdcspv::Id destXY = editor.AddConstant(
      rdcspv::OpSpecConstantComposite(float2Type, editor.MakeId(), {destX, destY}));

  editor.SetName(destXY, "destXY");

  rdcspv::Id destThreadIDX =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestThreadIDX);
  rdcspv::Id destThreadIDY =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestThreadIDY);
  rdcspv::Id destThreadIDZ =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestThreadIDZ);

  editor.SetName(destThreadIDX, "destThreadIDX");
  editor.SetName(destThreadIDY, "destThreadIDY");
  editor.SetName(destThreadIDZ, "destThreadIDZ");

  rdcspv::Id destThreadID = editor.AddConstant(rdcspv::OpSpecConstantComposite(
      uint3Type, editor.MakeId(), {destThreadIDX, destThreadIDY, destThreadIDZ}));

  editor.SetName(destThreadID, "destThreadID");

  rdcspv::Id destInstance =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestInstance);
  rdcspv::Id destVertex =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestVertex);
  rdcspv::Id destView =
      editor.AddSpecConstantImmediate<uint32_t>(0, (uint32_t)InputSpecConstant::DestView);

  editor.SetName(destInstance, "destInstance");
  editor.SetName(destVertex, "destVertex");
  editor.SetName(destView, "destView");

  rdcspv::Id ResultDataBaseType;

  uint32_t numLanes = 1;

  if(stage == ShaderStage::Pixel)
    numLanes = 4;

  if(threadScope & rdcspv::ThreadScope::Quad)
    numLanes = 4;

  // if we need full subgroup scope (and we have ballots to read the lanes) declare a subgroup's worth of data
  if(fullSubgroups)
    numLanes = RDCMAX(numLanes, maxSubgroupSize);

  // note we don't need to care about workgroup access - that is only possible on compute and we fill
  // in the rest of the workgroup without reading its inputs, since on compute the only subgroup data
  // we need is size + layout and we assume we can figure out the layout with one subgroup's worth of data

  {
    rdcarray<rdcspv::StructMember> members;

    members.push_back({float4Type, "pos", offsetof(rdcspv::ResultDataBase, pos)});
    members.push_back({uint32Type, "prim", offsetof(rdcspv::ResultDataBase, prim)});
    members.push_back({uint32Type, "sample", offsetof(rdcspv::ResultDataBase, sample)});
    members.push_back({uint32Type, "view", offsetof(rdcspv::ResultDataBase, view)});
    members.push_back({uint32Type, "valid", offsetof(rdcspv::ResultDataBase, valid)});
    members.push_back({floatType, "ddxDerivCheck", offsetof(rdcspv::ResultDataBase, ddxDerivCheck)});
    members.push_back({uint32Type, "quadLaneIndex", offsetof(rdcspv::ResultDataBase, quadLaneIndex)});
    members.push_back({uint32Type, "laneIndex", offsetof(rdcspv::ResultDataBase, laneIndex)});
    members.push_back({uint32Type, "subgroupSize", offsetof(rdcspv::ResultDataBase, subgroupSize)});
    members.push_back({uint4Type, "globalBallot", offsetof(rdcspv::ResultDataBase, globalBallot)});
    members.push_back({uint4Type, "electBallot", offsetof(rdcspv::ResultDataBase, electBallot)});
    members.push_back({uint4Type, "helperBallot", offsetof(rdcspv::ResultDataBase, helperBallot)});
    members.push_back({uint32Type, "numSubgroups", offsetof(rdcspv::ResultDataBase, numSubgroups)});

    // uint3 padding

    const uint32_t dataStart = (uint32_t)AlignUp(sizeof(rdcspv::ResultDataBase), sizeof(Vec4f));

    RDCASSERT((structStride % sizeof(Vec4f)) == 0);

    rdcspv::Id LaneDataArray = editor.AddType(rdcspv::OpTypeArray(
        editor.MakeId(), LaneDataStruct, editor.AddConstantImmediate<uint32_t>(numLanes)));
    editor.AddDecoration(rdcspv::OpDecorate(
        LaneDataArray, rdcspv::DecorationParam<rdcspv::Decoration::ArrayStride>(structStride)));

    members.push_back({LaneDataArray, "LaneData", dataStart});

    ResultDataBaseType = editor.DeclareStructType("ResultData", members);
  }

  rdcspv::Id ResultDataRTArray =
      editor.AddType(rdcspv::OpTypeRuntimeArray(editor.MakeId(), ResultDataBaseType));

  editor.AddDecoration(rdcspv::OpDecorate(
      ResultDataRTArray, rdcspv::DecorationParam<rdcspv::Decoration::ArrayStride>(
                             structStride * numLanes + sizeof(rdcspv::ResultDataBase))));

  rdcspv::Id bufBase =
      editor.DeclareStructType("__rd_HitStorage", {
                                                      {uint32Type, "hit_count", 0},
                                                      {uint32Type, "total_count", sizeof(uint32_t)},
                                                      {uint32Type, "dummy", sizeof(uint32_t) * 2},
                                                      // uint padding

                                                      {ResultDataRTArray, "hits", sizeof(Vec4f)},
                                                  });

  rdcspv::StorageClass bufferClass = editor.PrepareAddedBufferAccess();

  rdcpair<rdcspv::Id, rdcspv::Id> hitBuffer = editor.AddBufferVariable(
      newGlobals, bufBase, "__rd_HitBuffer", 0, (uint32_t)InputSpecConstant::Address, 0);

  RDCCOMPILE_ASSERT(
      uint32_t(InputSpecConstant::Address) + 1 == (uint32_t)InputSpecConstant::AddressMSB,
      "Address spec constant IDs must be contiguous with MSB second");

  rdcspv::Id float4InPtr =
      editor.DeclareType(rdcspv::Pointer(float4Type, rdcspv::StorageClass::Input));
  rdcspv::Id float4BufPtr = editor.DeclareType(rdcspv::Pointer(float4Type, bufferClass));

  rdcspv::Id uint32InPtr =
      editor.DeclareType(rdcspv::Pointer(uint32Type, rdcspv::StorageClass::Input));
  rdcspv::Id uint32BufPtr = editor.DeclareType(rdcspv::Pointer(uint32Type, bufferClass));
  rdcspv::Id uint4BufPtr = editor.DeclareType(rdcspv::Pointer(uint4Type, bufferClass));
  rdcspv::Id floatBufPtr = editor.DeclareType(rdcspv::Pointer(floatType, bufferClass));

  rdcspv::Id glsl450 = editor.ImportExtInst("GLSL.std.450");

  // allow pixel shaders to use fine derivatives
  if(stage == ShaderStage::Pixel)
    editor.AddCapability(rdcspv::Capability::DerivativeControl);

  // declare capabilities we might need
  if(threadScope & rdcspv::ThreadScope::Subgroup)
  {
    if(subgroupCapability == SubgroupCapability::None)
    {
      // nothing, ignore, this could only happen if the shader uses only EXT_shader_subgroup_vote
      // which we treat as degenerate
    }
    else if(subgroupCapability == SubgroupCapability::EXTBallot)
    {
      // this should already be present but let's be sure
      editor.AddCapability(rdcspv::Capability::SubgroupBallotKHR);
    }
    else if(subgroupCapability == SubgroupCapability::Vulkan1_1_NoBallot)
    {
      // this should also already be present
      editor.AddCapability(rdcspv::Capability::GroupNonUniform);
    }
    else if(subgroupCapability == SubgroupCapability::Vulkan1_1)
    {
      editor.AddCapability(rdcspv::Capability::GroupNonUniform);

      // add this, the shader might not have used it but we need it to read other lanes
      editor.AddCapability(rdcspv::Capability::GroupNonUniformBallot);
      editor.AddCapability(rdcspv::Capability::GroupNonUniformVote);
    }
  }

  rdcspv::Id vecNType[5] = {rdcspv::Id(), floatType, float2Type, float3Type, float4Type};
  rdcspv::Id quadSwizzleHelper[5] = {};

  if(stage == ShaderStage::Pixel)
  {
    for(uint32_t i = 1; i <= 4; i++)
    {
      quadSwizzleHelper[i] = AddQuadSwizzleHelper(editor, i);
    }
  }

  {
    rdcspv::OperationList ops;

    rdcspv::Id voidType = editor.DeclareType(rdcspv::scalar<void>());
    rdcspv::Id uintPtr = editor.DeclareType(rdcspv::Pointer(uint32Type, bufferClass));

    rdcspv::Id scope = editor.AddConstantImmediate<uint32_t>((uint32_t)rdcspv::Scope::Device);
    rdcspv::Id semantics =
        editor.AddConstantImmediate<uint32_t>((uint32_t)rdcspv::MemorySemantics::AcquireRelease);

    ops.add(rdcspv::OpFunction(voidType, entryID, rdcspv::FunctionControl::None,
                               editor.DeclareType(rdcspv::FunctionType(voidType, {}))));

    rdcspv::Id structPtr;

    ops.add(rdcspv::OpLabel(editor.MakeId()));
    {
      structPtr = editor.LoadBufferVariable(ops, hitBuffer);

      // we store ddx as a derivative check - it is expected to be 1.0 so store that as fixed for other stages
      rdcspv::Id fragCoord, ddxDerivativeCheck = editor.AddConstantImmediate<float>(1.0f);
      rdcspv::Id laneIndex;

      // identify the candidate thread in a stage-specific way
      rdcspv::Id candidateThread;

      // prepare stage-specific inputs and condition
      if(stage == ShaderStage::Vertex)
      {
        // we should only be fetching data like this for full subgroups
        RDCASSERT(fullSubgroups);

        rdcspv::Id vert = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                     rdcspv::BuiltIn::VertexIndex, uint32Type);
        rdcspv::Id inst = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                     rdcspv::BuiltIn::InstanceIndex, uint32Type);

        rdcspv::Id equalVert = ops.add(rdcspv::OpIEqual(boolType, editor.MakeId(), vert, destVertex));
        rdcspv::Id equalInstance =
            ops.add(rdcspv::OpIEqual(boolType, editor.MakeId(), inst, destInstance));

        candidateThread =
            ops.add(rdcspv::OpLogicalAnd(boolType, editor.MakeId(), equalVert, equalInstance));

        if(useViewIndex)
        {
          rdcspv::Id view = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                       rdcspv::BuiltIn::ViewIndex, uint32Type);
          rdcspv::Id equalView = ops.add(rdcspv::OpIEqual(boolType, editor.MakeId(), view, destView));
          candidateThread =
              ops.add(rdcspv::OpLogicalAnd(boolType, editor.MakeId(), candidateThread, equalView));
        }
      }
      else if(stage == ShaderStage::Pixel)
      {
        fragCoord = editor.AddBuiltinInputLoad(ops, newGlobals, stage, rdcspv::BuiltIn::FragCoord,
                                               float4Type);

        ddxDerivativeCheck = ops.add(rdcspv::OpDPdx(float4Type, editor.MakeId(), fragCoord));
        editor.SetName(ddxDerivativeCheck, "ddxDerivativeCheck");
        ddxDerivativeCheck =
            ops.add(rdcspv::OpCompositeExtract(floatType, editor.MakeId(), ddxDerivativeCheck, {0}));
        editor.SetName(ddxDerivativeCheck, "ddxDerivativeCheck_x");

        // grab x and y
        rdcspv::Id fragXY = ops.add(
            rdcspv::OpVectorShuffle(float2Type, editor.MakeId(), fragCoord, fragCoord, {0, 1}));

        /*
        // figure out the TL pixel's coords and calculate our index relative to it. Assume even top
        // left (towards 0,0) though the spec does not guarantee this is the actual quad
        int yTL = y & (~1);

        // get the index of our desired pixel
        */

        rdcspv::Id mask = editor.AddConstantImmediate<uint32_t>(1);

        // int x01 = x & 1;
        rdcspv::Id xInt =
            ops.add(rdcspv::OpCompositeExtract(floatType, editor.MakeId(), fragXY, {0}));
        xInt = ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), xInt));
        rdcspv::Id x01 = ops.add(rdcspv::OpBitwiseAnd(uint32Type, editor.MakeId(), xInt, mask));

        // int y01 = y & 1;
        rdcspv::Id yInt =
            ops.add(rdcspv::OpCompositeExtract(floatType, editor.MakeId(), fragXY, {1}));
        yInt = ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), yInt));
        rdcspv::Id y01 = ops.add(rdcspv::OpBitwiseAnd(uint32Type, editor.MakeId(), yInt, mask));

        // int destIdx = x01 + 2 * y01;
        rdcspv::Id sum = ops.add(rdcspv::OpIMul(uint32Type, editor.MakeId(),
                                                editor.AddConstantImmediate<uint32_t>(2), y01));
        ops.add(rdcspv::OpIAdd(uint32Type, quadLaneIndex, sum, x01));
        laneIndex = quadLaneIndex;
        editor.SetName(quadLaneIndex, "quadLaneIndex");

        // bool candidateThread = all(abs(gl_FragCoord.xy - dest.xy) < 0.5f);
        rdcspv::Id bool2Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<bool>(), 2));

        // subtract frag coord from the destination co-ord in x-y to get relative
        rdcspv::Id fragXYRelative =
            ops.add(rdcspv::OpFSub(float2Type, editor.MakeId(), fragXY, destXY));

        // abs()
        rdcspv::Id fragXYAbs = ops.add(rdcspv::OpGLSL450(
            float2Type, editor.MakeId(), glsl450, rdcspv::GLSLstd450::FAbs, {fragXYRelative}));

        rdcspv::Id half = editor.AddConstantImmediate<float>(0.5f);
        rdcspv::Id threshold = editor.AddConstant(
            rdcspv::OpConstantComposite(float2Type, editor.MakeId(), {half, half}));

        // less than 0.5
        rdcspv::Id inPixelXY =
            ops.add(rdcspv::OpFOrdLessThan(bool2Type, editor.MakeId(), fragXYAbs, threshold));

        // both less than 0.5
        candidateThread = ops.add(rdcspv::OpAll(boolType, editor.MakeId(), inPixelXY));
      }
      else if(stage == ShaderStage::Compute || stage == ShaderStage::Task ||
              stage == ShaderStage::Mesh)
      {
        // we should only be fetching data like this for full subgroups
        RDCASSERT(fullSubgroups);

        rdcspv::Id globalThread = editor.AddBuiltinInputLoad(
            ops, newGlobals, stage, rdcspv::BuiltIn::GlobalInvocationId, uint3Type);

        rdcspv::Id bool3Type = editor.DeclareType(rdcspv::Vector(rdcspv::scalar<bool>(), 3));
        rdcspv::Id equal3 =
            ops.add(rdcspv::OpIEqual(bool3Type, editor.MakeId(), globalThread, destThreadID));

        candidateThread = ops.add(rdcspv::OpAll(boolType, editor.MakeId(), equal3));
      }

      rdcspv::Id quadIdxConst[4] = {
          editor.AddConstantImmediate<uint32_t>(0),
          editor.AddConstantImmediate<uint32_t>(1),
          editor.AddConstantImmediate<uint32_t>(2),
          editor.AddConstantImmediate<uint32_t>(3),
      };

      // load all data per-thread and calculate quad swizzled neighbour data as needed
      for(laneValue &val : laneValues)
      {
        ops.append(val.loadOps);

        // for pixel shaders we always need to grab quad swizzled data.
        // we skip this for values we classify as flat as well as for the magic isHelper/quadLaneIndex
        // which are handled specially and will be fixed up later when we go to store these
        if(stage == ShaderStage::Pixel && val.base != isHelper && val.base != quadLaneIndex &&
           !val.flat)
        {
          val.quadSwizzledData.resize(4);

          const rdcspv::DataType &dataType = editor.GetDataType(val.type);

          if(dataType.IsU32())
          {
            for(uint32_t q = 0; q < 4; q++)
            {
              rdcspv::Id valQ = val.base;
              valQ = ops.add(rdcspv::OpConvertUToF(floatType, editor.MakeId(), valQ));
              valQ = ops.add(rdcspv::OpFunctionCall(floatType, editor.MakeId(), quadSwizzleHelper[1],
                                                    {valQ, quadLaneIndex, quadIdxConst[q]}));
              // named as convenience because spirv-cross declares a variable here and then does the cast at the usage
              editor.SetName(valQ, StringFormat::Fmt("%s_swiz%u", val.name.c_str(), q));

              valQ = ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), valQ));
              editor.SetName(valQ, StringFormat::Fmt("%s_swiz%u_u", val.name.c_str(), q));

              val.quadSwizzledData[q] = valQ;
            }
          }
          else
          {
            // all other inputs that aren't uint32 should be floats, otherwise they should have been marked as flat
            RDCASSERT(dataType.scalar().type == rdcspv::Op::TypeFloat);

            uint32_t width = RDCMAX(1U, dataType.vector().count);

            for(uint32_t q = 0; q < 4; q++)
            {
              val.quadSwizzledData[q] = ops.add(
                  rdcspv::OpFunctionCall(vecNType[width], editor.MakeId(), quadSwizzleHelper[width],
                                         {val.base, quadLaneIndex, quadIdxConst[q]}));
              editor.SetName(val.quadSwizzledData[q],
                             StringFormat::Fmt("%s_swiz%u", val.name.c_str(), q));
            }
          }
        }
      }

      rdcspv::Id subgroupSize, numSubgroups, globalBallot, electBallot, helperBallot;

      // if we are doing even minimal subgroups, read the subgroup-relative lane index and subgroup size
      if(minimalSubgroups || fullSubgroups)
      {
        if(subgroupCapability == SubgroupCapability::EXTBallot)
        {
          globalBallot = ops.add(rdcspv::OpSubgroupBallotKHR(
              uint4Type, editor.MakeId(), editor.AddConstantImmediate<bool>(true)));
          electBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));

          if(stage == ShaderStage::Pixel)
          {
            helperBallot = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), isHelper,
                                                       editor.AddConstantImmediate<uint32_t>(0)));
            helperBallot =
                ops.add(rdcspv::OpSubgroupBallotKHR(uint4Type, editor.MakeId(), helperBallot));
          }
          else
          {
            helperBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));
          }
        }
        else
        {
          globalBallot = ops.add(rdcspv::OpGroupNonUniformBallot(
              uint4Type, editor.MakeId(), subgroupScope, editor.AddConstantImmediate<bool>(true)));
          electBallot =
              ops.add(rdcspv::OpGroupNonUniformElect(boolType, editor.MakeId(), subgroupScope));
          electBallot = ops.add(rdcspv::OpGroupNonUniformBallot(uint4Type, editor.MakeId(),
                                                                subgroupScope, electBallot));

          if(stage == ShaderStage::Pixel)
          {
            helperBallot = ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), isHelper,
                                                       editor.AddConstantImmediate<uint32_t>(0)));
            helperBallot = ops.add(rdcspv::OpGroupNonUniformBallot(uint4Type, editor.MakeId(),
                                                                   subgroupScope, helperBallot));
          }
          else
          {
            helperBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));
          }
        }

        laneIndex = editor.AddBuiltinInputLoad(
            ops, newGlobals, stage, rdcspv::BuiltIn::SubgroupLocalInvocationId, uint32Type);
        subgroupSize = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                  rdcspv::BuiltIn::SubgroupSize, uint32Type);
        editor.SetName(laneIndex, "laneIndex");
        editor.SetName(subgroupSize, "subgroupSize");

        // subgroup ID & num subgroups is only available for compute
        if(stage == ShaderStage::Compute || stage == ShaderStage::Task || stage == ShaderStage::Mesh)
        {
          numSubgroups = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                    rdcspv::BuiltIn::NumSubgroups, uint32Type);
          editor.SetName(numSubgroups, "numSubgroups");
        }
        else
        {
          numSubgroups = editor.AddConstantImmediate<uint32_t>(0);
        }
      }
      else
      {
        globalBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));
        electBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));
        helperBallot = editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()));
        subgroupSize = editor.AddConstantImmediate<uint32_t>(0);
        numSubgroups = editor.AddConstantImmediate<uint32_t>(0);
      }
      editor.SetName(globalBallot, "globalBallot");

      // in a pixel shader we need to take extra steps to ensure we get helper data, and it depends
      // on if we're fetching subgroups or not.
      // if we're not fetching subgroups, we always fetch all 4 helpers and store them since that's
      // all our data. if we ARE fetching subgroups, helper lanes will not write their own data so
      // we do that from the candidate thread (only for the helper lanes, conditionally). We also
      // store into the lane index of each quad with subgroups, as opposed to just 0-3 for a plain
      // quad.
      // if we're not in a pixel shader we don't do any of this
      rdcspv::Id isHelperPerQuad[4] = {};
      rdcspv::Id shouldStoreHelperPerQuad[4] = {};
      rdcspv::Id quadLaneStoreIdx[4] = {};

      if(stage == ShaderStage::Pixel)
      {
        // calculate the quadId that we need for pixels, the top-left thread's lane index
        rdcspv::Id quadIdSwizzle =
            ops.add(rdcspv::OpConvertUToF(floatType, editor.MakeId(), laneIndex));
        quadIdSwizzle =
            ops.add(rdcspv::OpFunctionCall(floatType, editor.MakeId(), quadSwizzleHelper[1],
                                           {quadIdSwizzle, quadLaneIndex, quadIdxConst[0]}));

        quadIdSwizzle = ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), quadIdSwizzle));
        // add offset so that quad IDs are always non-zero
        quadId = ops.add(rdcspv::OpIAdd(uint32Type, quadId, quadIdSwizzle,
                                        editor.AddConstantImmediate<uint32_t>(10000)));

        for(uint32_t q = 0; q < 4; q++)
        {
          isHelperPerQuad[q] = ops.add(rdcspv::OpConvertUToF(floatType, editor.MakeId(), isHelper));
          isHelperPerQuad[q] =
              ops.add(rdcspv::OpFunctionCall(floatType, editor.MakeId(), quadSwizzleHelper[1],
                                             {isHelperPerQuad[q], quadLaneIndex, quadIdxConst[q]}));
          isHelperPerQuad[q] =
              ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), isHelperPerQuad[q]));
        }

        if(fullSubgroups)
        {
          for(uint32_t q = 0; q < 4; q++)
          {
            shouldStoreHelperPerQuad[q] =
                ops.add(rdcspv::OpINotEqual(boolType, editor.MakeId(), isHelperPerQuad[q],
                                            editor.AddConstantImmediate<uint32_t>(0)));

            quadLaneStoreIdx[q] =
                ops.add(rdcspv::OpConvertUToF(floatType, editor.MakeId(), laneIndex));
            quadLaneStoreIdx[q] = ops.add(
                rdcspv::OpFunctionCall(floatType, editor.MakeId(), quadSwizzleHelper[1],
                                       {quadLaneStoreIdx[q], quadLaneIndex, quadIdxConst[q]}));
            quadLaneStoreIdx[q] =
                ops.add(rdcspv::OpConvertFToU(uint32Type, editor.MakeId(), quadLaneStoreIdx[q]));

            editor.SetName(isHelperPerQuad[q], StringFormat::Fmt("isHelper%u", q));
            editor.SetName(shouldStoreHelperPerQuad[q], StringFormat::Fmt("shouldStore%u", q));
            editor.SetName(quadLaneStoreIdx[q], StringFormat::Fmt("quadLaneStoreIdx%u", q));
          }
        }
        else
        {
          for(uint32_t q = 0; q < 4; q++)
          {
            shouldStoreHelperPerQuad[q] = editor.AddConstantImmediate<bool>(true);
            quadLaneStoreIdx[q] = quadIdxConst[q];
          }
        }
      }

      // get a pointer to buffer.hit_count
      rdcspv::Id hit_count = ops.add(rdcspv::OpAccessChain(
          uintPtr, editor.MakeId(), structPtr, {editor.AddConstantImmediate<uint32_t>(0)}));

      // get a pointer to buffer.total_count
      rdcspv::Id total_count = ops.add(rdcspv::OpAccessChain(
          uintPtr, editor.MakeId(), structPtr, {editor.AddConstantImmediate<uint32_t>(1)}));

      editor.SetName(candidateThread, "candidateThread");

      // if we are fetching full subgroups in a pixel shader, we need to know which threads are quad
      // neighbours of the candidate so we can write their quad lane index properly
      rdcspv::Id candidateThreadInQuad;
      if(fullSubgroups && stage == ShaderStage::Pixel)
      {
        rdcspv::Id zeroF = editor.AddConstantImmediate<float>(0.0f);

        // we do a simple check here - if candidateThread is true, or any ddx/ddy is non-zero, we're
        // in the candidate quad

        rdcspv::Id candidateThreadF = ops.add(rdcspv::OpSelect(
            uint32Type, editor.MakeId(), candidateThread, editor.AddConstantImmediate<uint32_t>(1),
            editor.AddConstantImmediate<uint32_t>(0)));
        candidateThreadF =
            ops.add(rdcspv::OpConvertUToF(floatType, editor.MakeId(), candidateThreadF));

        rdcspv::Id candidateThreadDDXFine =
            ops.add(rdcspv::OpDPdxFine(floatType, editor.MakeId(), candidateThreadF));
        candidateThreadDDXFine = ops.add(
            rdcspv::OpFOrdGreaterThan(boolType, editor.MakeId(), candidateThreadDDXFine, zeroF));

        rdcspv::Id candidateThreadDDYFine =
            ops.add(rdcspv::OpDPdyFine(floatType, editor.MakeId(), candidateThreadF));
        candidateThreadDDYFine = ops.add(
            rdcspv::OpFOrdGreaterThan(boolType, editor.MakeId(), candidateThreadDDYFine, zeroF));

        rdcspv::Id candidateThreadDDXCoarse =
            ops.add(rdcspv::OpDPdxCoarse(floatType, editor.MakeId(), candidateThreadF));
        candidateThreadDDXCoarse = ops.add(
            rdcspv::OpFOrdGreaterThan(boolType, editor.MakeId(), candidateThreadDDXCoarse, zeroF));

        rdcspv::Id candidateThreadDDYCoarse =
            ops.add(rdcspv::OpDPdyCoarse(floatType, editor.MakeId(), candidateThreadF));
        candidateThreadDDYCoarse = ops.add(
            rdcspv::OpFOrdGreaterThan(boolType, editor.MakeId(), candidateThreadDDYCoarse, zeroF));

        candidateThreadInQuad = ops.add(rdcspv::OpLogicalOr(
            boolType, editor.MakeId(), candidateThread, candidateThreadDDXFine));
        candidateThreadInQuad = ops.add(rdcspv::OpLogicalOr(
            boolType, editor.MakeId(), candidateThreadInQuad, candidateThreadDDYFine));
        candidateThreadInQuad = ops.add(rdcspv::OpLogicalOr(
            boolType, editor.MakeId(), candidateThreadInQuad, candidateThreadDDXCoarse));
        candidateThreadInQuad = ops.add(rdcspv::OpLogicalOr(
            boolType, editor.MakeId(), candidateThreadInQuad, candidateThreadDDYCoarse));
        editor.SetName(candidateThreadInQuad, "candidateThreadInQuad");
      }

      rdcarray<rdcspv::Id> killLabels;
      killLabels.push_back(editor.MakeId());
      rdcspv::Id writeLabel = editor.MakeId();

      rdcspv::Id writeCondition = candidateThread;

      // if we're doing proper subgroup readback, keep the whole subgroup, otherwise branch non-uniformly
      if(fullSubgroups)
      {
        if(subgroupCapability == SubgroupCapability::Vulkan1_1)
        {
          writeCondition = ops.add(rdcspv::OpGroupNonUniformAny(boolType, editor.MakeId(),
                                                                subgroupScope, candidateThread));
        }
        else
        {
          // KHR path, emulate a vote with any(ballot() != 0u) so we don't depend on the vote
          // extension - we probably can, but don't have to
          writeCondition =
              ops.add(rdcspv::OpSubgroupBallotKHR(uint4Type, editor.MakeId(), candidateThread));
          writeCondition = ops.add(rdcspv::OpINotEqual(
              bool4Type, editor.MakeId(), writeCondition,
              editor.AddConstant(rdcspv::OpConstantNull(uint4Type, editor.MakeId()))));
          writeCondition = ops.add(rdcspv::OpAny(boolType, editor.MakeId(), writeCondition));
        }
      }

      ops.add(rdcspv::OpSelectionMerge(killLabels.back(), rdcspv::SelectionControl::None));
      ops.add(rdcspv::OpBranchConditional(writeCondition, writeLabel, killLabels.back()));

      ops.add(rdcspv::OpLabel(writeLabel));

      // for pixel shaders with subgroups, ensure we mask off helper lanes from the subgroup so they
      // don't take part in the elect
      if(fullSubgroups && stage == ShaderStage::Pixel)
      {
        killLabels.push_back(editor.MakeId());
        writeLabel = editor.MakeId();
        rdcspv::Id helperCondition = ops.add(rdcspv::OpIEqual(
            boolType, editor.MakeId(), isHelper, editor.AddConstantImmediate<uint32_t>(0)));
        ops.add(rdcspv::OpSelectionMerge(killLabels.back(), rdcspv::SelectionControl::None));
        ops.add(rdcspv::OpBranchConditional(helperCondition, writeLabel, killLabels.back()));
        ops.add(rdcspv::OpLabel(writeLabel));
      }

      rdcspv::Id slotAllocLabel = editor.MakeId(), slotMergeLabel = editor.MakeId();

      // for subgroups the whole subgroup is in here, ensure we only alloc a slot with one lane
      if(fullSubgroups)
      {
        rdcspv::Id nonHelperElected;

        if(subgroupCapability == SubgroupCapability::Vulkan1_1)
          nonHelperElected =
              ops.add(rdcspv::OpGroupNonUniformElect(boolType, editor.MakeId(), subgroupScope));
        else
          nonHelperElected = ops.add(rdcspv::OpSubgroupFirstInvocationKHR(
              boolType, editor.MakeId(), editor.AddConstantImmediate<bool>(true)));
        ops.add(rdcspv::OpSelectionMerge(slotMergeLabel, rdcspv::SelectionControl::None));
        ops.add(rdcspv::OpBranchConditional(nonHelperElected, slotAllocLabel, slotMergeLabel));
      }
      else
      {
        ops.add(rdcspv::OpBranch(slotAllocLabel));
      }

      ops.add(rdcspv::OpLabel(slotAllocLabel));

      // increment total_count
      ops.add(rdcspv::OpAtomicIAdd(uint32Type, editor.MakeId(), total_count, scope, semantics,
                                   editor.AddConstantImmediate<uint32_t>(1)));

      // allocate a slot with atomic add
      rdcspv::Id slot =
          ops.add(rdcspv::OpAtomicIAdd(uint32Type, editor.MakeId(), hit_count, scope, semantics,
                                       editor.AddConstantImmediate<uint32_t>(1)));

      editor.SetName(slot, "slotAlloc");

      ops.add(rdcspv::OpBranch(slotMergeLabel));

      ops.add(rdcspv::OpLabel(slotMergeLabel));

      // now if we're in a subgroup we need to broadcast the slot to the whole group, and also OpPhi
      // the previous slot depending on where we got it from
      if(fullSubgroups)
      {
        slot = ops.add(rdcspv::OpPhi(
            uint32Type, editor.MakeId(),
            {{slot, slotAllocLabel}, {editor.AddConstantImmediate<uint32_t>(0U), writeLabel}}));
        editor.SetName(slot, "slotToBroadcast");

        if(subgroupCapability == SubgroupCapability::Vulkan1_1)
          slot = ops.add(rdcspv::OpGroupNonUniformBroadcastFirst(uint32Type, editor.MakeId(),
                                                                 subgroupScope, slot));
        else
          slot = ops.add(rdcspv::OpSubgroupFirstInvocationKHR(uint32Type, editor.MakeId(), slot));
        editor.SetName(slot, "slot");
      }

      rdcspv::Id inRange = ops.add(rdcspv::OpULessThan(boolType, editor.MakeId(), slot, arrayLength));

      killLabels.push_back(editor.MakeId());
      writeLabel = editor.MakeId();
      ops.add(rdcspv::OpSelectionMerge(killLabels.back(), rdcspv::SelectionControl::None));
      ops.add(rdcspv::OpBranchConditional(inRange, writeLabel, killLabels.back()));
      ops.add(rdcspv::OpLabel(writeLabel));

      rdcspv::Id hitptr = editor.DeclareType(rdcspv::Pointer(ResultDataBaseType, bufferClass));

      // get a pointer to the hit for our slot
      rdcspv::Id hit = ops.add(rdcspv::OpAccessChain(
          hitptr, editor.MakeId(), structPtr, {editor.AddConstantImmediate<uint32_t>(3), slot}));

      // store fixed properties. In the subgroup case this needs to be conditional for only the candidate thread
      rdcspv::Id fixedDataLabel = editor.MakeId(), fixedDataMerge = editor.MakeId();

      if(fullSubgroups)
      {
        ops.add(rdcspv::OpSelectionMerge(fixedDataMerge, rdcspv::SelectionControl::None));
        ops.add(rdcspv::OpBranchConditional(candidateThread, fixedDataLabel, fixedDataMerge));
      }
      else
      {
        ops.add(rdcspv::OpBranch(fixedDataLabel));
      }

      ops.add(rdcspv::OpLabel(fixedDataLabel));

      rdcspv::Id storePtr =
          ops.add(rdcspv::OpAccessChain(float4BufPtr, editor.MakeId(), hit,
                                        {editor.AddConstantImmediate<uint32_t>(ResultBase_pos)}));
      if(fragCoord != rdcspv::Id())
        ops.add(rdcspv::OpStore(storePtr, fragCoord, alignedAccess));

      rdcspv::Id primitiveID;
      if(usePrimitiveID)
      {
        primitiveID = editor.AddBuiltinInputLoad(ops, newGlobals, stage,
                                                 rdcspv::BuiltIn::PrimitiveId, uint32Type);
        editor.AddCapability(rdcspv::Capability::Geometry);
      }
      else
      {
        primitiveID = editor.AddConstantImmediate<uint32_t>(0);
      }

      storePtr =
          ops.add(rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                        {editor.AddConstantImmediate<uint32_t>(ResultBase_prim)}));
      ops.add(rdcspv::OpStore(storePtr, primitiveID, alignedAccess));

      rdcspv::Id sampleIndex;
      if(useSampleID)
      {
        sampleIndex = editor.AddBuiltinInputLoad(ops, newGlobals, stage, rdcspv::BuiltIn::SampleId,
                                                 uint32Type);
        editor.AddCapability(rdcspv::Capability::SampleRateShading);
      }
      else
      {
        sampleIndex = editor.AddConstantImmediate<uint32_t>(0);
      }

      storePtr =
          ops.add(rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                        {editor.AddConstantImmediate<uint32_t>(ResultBase_sample)}));
      ops.add(rdcspv::OpStore(storePtr, sampleIndex, alignedAccess));

      rdcspv::Id viewIndex;
      if(useViewIndex)
      {
        viewIndex = editor.AddBuiltinInputLoad(ops, newGlobals, stage, rdcspv::BuiltIn::ViewIndex,
                                               uint32Type);
        editor.AddCapability(rdcspv::Capability::MultiView);
        editor.AddExtension("SPV_KHR_multiview");
      }
      else
      {
        viewIndex = editor.AddConstantImmediate<uint32_t>(0);
      }

      storePtr =
          ops.add(rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                        {editor.AddConstantImmediate<uint32_t>(ResultBase_view)}));
      ops.add(rdcspv::OpStore(storePtr, viewIndex, alignedAccess));

      storePtr =
          ops.add(rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                        {editor.AddConstantImmediate<uint32_t>(ResultBase_valid)}));
      ops.add(rdcspv::OpStore(storePtr, editor.AddConstantImmediate(validMagicNumber), alignedAccess));

      // store derivative health check for pixel shaders
      storePtr = ops.add(
          rdcspv::OpAccessChain(floatBufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_ddxDerivCheck)}));
      ops.add(rdcspv::OpStore(storePtr, ddxDerivativeCheck, alignedAccess));

      // store the quadLaneIndex (in case it's different to laneIndex)
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_quadLaneIndex)}));
      if(quadLaneIndex != rdcspv::Id())
        ops.add(rdcspv::OpStore(storePtr, quadLaneIndex, alignedAccess));

      // store the laneIndex
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_laneIndex)}));
      ops.add(rdcspv::OpStore(storePtr, laneIndex, alignedAccess));

      // if we have them, store subgroup properties, if they're not present they will be 0
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_subgroupSize)}));
      ops.add(rdcspv::OpStore(storePtr, subgroupSize, alignedAccess));
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint4BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_globalBallot)}));
      ops.add(rdcspv::OpStore(storePtr, globalBallot, alignedAccess));
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint4BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_electBallot)}));
      ops.add(rdcspv::OpStore(storePtr, electBallot, alignedAccess));
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint4BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_helperBallot)}));
      ops.add(rdcspv::OpStore(storePtr, helperBallot, alignedAccess));
      storePtr = ops.add(
          rdcspv::OpAccessChain(uint32BufPtr, editor.MakeId(), hit,
                                {editor.AddConstantImmediate<uint32_t>(ResultBase_numSubgroups)}));
      ops.add(rdcspv::OpStore(storePtr, numSubgroups, alignedAccess));

      // merge after doing the fixed data section
      ops.add(rdcspv::OpBranch(fixedDataMerge));
      ops.add(rdcspv::OpLabel(fixedDataMerge));

      rdcspv::Id LaneDataPtrType = editor.DeclareType(rdcspv::Pointer(LaneDataStruct, bufferClass));

      // now we conditionally store each helper lane. Only relevant for pixel shaders but we need
      // all helper lanes for all active lanes to ensure we can get derivatives for any of them
      if(stage == ShaderStage::Pixel)
      {
        for(uint32_t q = 0; q < 4; q++)
        {
          rdcspv::Id doHelperLabel = editor.MakeId(), skipHelperLabel = editor.MakeId();

          ops.add(rdcspv::OpSelectionMerge(skipHelperLabel, rdcspv::SelectionControl::None));
          ops.add(rdcspv::OpBranchConditional(shouldStoreHelperPerQuad[q], doHelperLabel,
                                              skipHelperLabel));
          ops.add(rdcspv::OpLabel(doHelperLabel));

          rdcspv::Id outputPtr = ops.add(rdcspv::OpAccessChain(
              LaneDataPtrType, editor.MakeId(), hit,
              {editor.AddConstantImmediate<uint32_t>(ResultBase_firstUser), quadLaneStoreIdx[q]}));

          for(laneValue &val : laneValues)
          {
            rdcspv::Id valueType = val.type;
            if(valueType == boolType)
              valueType = uint32Type;
            rdcspv::Id ptrType = editor.DeclareType(rdcspv::Pointer(valueType, bufferClass));

            rdcspv::Id valPtr = ops.add(rdcspv::OpAccessChain(
                ptrType, editor.MakeId(), outputPtr,
                {editor.AddConstantImmediate<uint32_t>((uint32_t)val.structIndex)}));

            if(val.base == isHelper)
            {
              ops.add(rdcspv::OpStore(valPtr, isHelperPerQuad[q], alignedAccess));
            }
            else if(val.base == quadLaneIndex)
            {
              ops.add(rdcspv::OpStore(valPtr, quadIdxConst[q], alignedAccess));
            }
            else if(val.flat)
            {
              ops.add(rdcspv::OpStore(valPtr, val.base, alignedAccess));
            }
            else
            {
              RDCASSERT(!val.quadSwizzledData.empty());
              ops.add(rdcspv::OpStore(valPtr, val.quadSwizzledData[q], alignedAccess));
            }
          }

          ops.add(rdcspv::OpBranch(skipHelperLabel));
          ops.add(rdcspv::OpLabel(skipHelperLabel));
        }
      }

      // if we have full subgroups, each subgroup now writes its own data here, if we are in a
      // non-pixel shader without subgroups we store the single thread's data here.
      // the non-subgroup pixel shader case is handled above in the helper lanes (which will all store)
      if(fullSubgroups || stage != ShaderStage::Pixel)
      {
        rdcspv::Id idx;

        if(fullSubgroups)
          idx = laneIndex;
        else
          idx = editor.AddConstantImmediate<uint32_t>(0U);

        rdcspv::Id outputPtr = ops.add(rdcspv::OpAccessChain(
            LaneDataPtrType, editor.MakeId(), hit,
            {editor.AddConstantImmediate<uint32_t>(ResultBase_firstUser), idx}));

        for(laneValue &val : laneValues)
        {
          rdcspv::Id valueType = val.type;
          if(valueType == boolType)
            valueType = uint32Type;
          rdcspv::Id ptrType = editor.DeclareType(rdcspv::Pointer(valueType, bufferClass));

          rdcspv::Id valPtr = ops.add(rdcspv::OpAccessChain(
              ptrType, editor.MakeId(), outputPtr,
              {editor.AddConstantImmediate<uint32_t>((uint32_t)val.structIndex)}));

          ops.add(rdcspv::OpStore(valPtr, val.base, alignedAccess));
        }
      }

      // join up with the early-outs we did, in reverse order
      for(size_t i = 0; i < killLabels.size(); i++)
      {
        rdcspv::Id label = killLabels[killLabels.size() - 1 - i];
        ops.add(rdcspv::OpBranch(label));
        ops.add(rdcspv::OpLabel(label));
      }
    }

    // we want to "call" the original function to ensure the compiler does hopefully as close
    // codegen as possible to the original but we don't want to actually execute it. To do this we
    // use an atomic max with a dummy value and only call the function if the value is *larger* -
    // the compiler can't know what value was pre-existing in the buffer (though we know it was
    // zero) so it can't eliminate either branch, but in practice we will always return

    rdcspv::Id trueLabel = editor.MakeId();
    rdcspv::Id falseLabel = editor.MakeId();

    // get a pointer to buffer.dummy
    rdcspv::Id dummy = ops.add(rdcspv::OpAccessChain(uintPtr, editor.MakeId(), structPtr,
                                                     {editor.AddConstantImmediate<uint32_t>(2)}));

    dummy = ops.add(rdcspv::OpAtomicUMax(uint32Type, editor.MakeId(), dummy, scope, semantics,
                                         editor.AddConstantImmediate<uint32_t>(1)));
    editor.SetName(dummy, "dummy");
    rdcspv::Id dummyCompare = ops.add(rdcspv::OpULessThan(
        boolType, editor.MakeId(), dummy, editor.AddConstantImmediate<uint32_t>(2)));

    ops.add(rdcspv::OpSelectionMerge(falseLabel, rdcspv::SelectionControl::None));
    ops.add(rdcspv::OpBranchConditional(dummyCompare, trueLabel, falseLabel));

    ops.add(rdcspv::OpLabel(trueLabel));

    //  don't return, kill. This makes it well-defined that we don't write anything to our outputs
    if(ShaderStage(shadRefl.stageIndex) == ShaderStage::Pixel)
      ops.add(rdcspv::OpKill());
    else
      ops.add(rdcspv::OpReturn());

    ops.add(rdcspv::OpLabel(falseLabel));

    ops.add(rdcspv::OpFunctionCall(voidType, editor.MakeId(), originalEntry));

    ops.add(rdcspv::OpReturn());

    ops.add(rdcspv::OpFunctionEnd());

    editor.AddFunction(ops);
  }

  editor.AddEntryGlobals(entryID, newGlobals);
}

rdcpair<uint32_t, uint32_t> GetAlignAndOutputSize(VulkanCreationInfo::ShaderModuleReflection &shadRefl)
{
  uint32_t paramAlign = 16;

  for(const SigParameter &sig : shadRefl.refl->inputSignature)
  {
    if(VarTypeByteSize(sig.varType) * sig.compCount > paramAlign)
      paramAlign = 32;
  }

  // conservatively calculate structure stride with full amount for every input element
  uint32_t structStride = (uint32_t)shadRefl.refl->inputSignature.size() * paramAlign;

  if(shadRefl.refl->stage == ShaderStage::Vertex)
    structStride += sizeof(rdcspv::VertexLaneData);
  else if(shadRefl.refl->stage == ShaderStage::Pixel)
    structStride += sizeof(rdcspv::PixelLaneData);
  else if(shadRefl.refl->stage == ShaderStage::Compute ||
          shadRefl.refl->stage == ShaderStage::Task || shadRefl.refl->stage == ShaderStage::Mesh)
    structStride += sizeof(rdcspv::ComputeLaneData);

  if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
  {
    structStride += sizeof(rdcspv::SubgroupLaneData);
  }

  return {paramAlign, structStride};
}

VkDescriptorSetLayoutBinding MakeNewBinding(VkShaderStageFlagBits stage)
{
  return {
      0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, NULL,
  };
}

void VulkanReplay::CalculateSubgroupProperties(uint32_t &maxSubgroupSize,
                                               SubgroupCapability &subgroupCapability)
{
  maxSubgroupSize = 4;

  // if we don't have subgroup ballots we assume we have no real meaningful subgroup capabilities at
  // all except for 'basic'. The only thing basic lets you do is fetch the subgroup ID, and
  // determine which lane is the first active (OpGroupNonUniformElect).
  // in this case we effectively consider it non-subgroup and just read those values directly to
  // fill in, but otherwise simulate as if there were no subgroup use.

  // for our purposes vulkan 1.1 fully deprecated the old EXT_shader_subgroup_* pair of extensions
  // as the only thing that wasn't deprecated was a non-constant broadcast ID which we don't need
  if(m_pDriver->GetExtensions(NULL).vulkanVersion >= VK_API_VERSION_1_1)
  {
    VkPhysicalDeviceSubgroupProperties subProps = {
        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES,
    };

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

    maxSubgroupSize = subProps.subgroupSize;
    subgroupCapability = SubgroupCapability::Vulkan1_1_NoBallot;
    const VkSubgroupFeatureFlags requiredFlags =
        (VK_SUBGROUP_FEATURE_BALLOT_BIT | VK_SUBGROUP_FEATURE_VOTE_BIT);
    if((subProps.supportedOperations & requiredFlags) == requiredFlags)
      subgroupCapability = SubgroupCapability::Vulkan1_1;

    if(m_pDriver->GetExtensions(NULL).ext_EXT_subgroup_size_control)
    {
      VkPhysicalDeviceSubgroupSizeControlProperties subSizeProps = {
          VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES,
      };
      availBase.pNext = &subSizeProps;
      m_pDriver->vkGetPhysicalDeviceProperties2(m_pDriver->GetPhysDev(), &availBase);

      // use new upper bound in case it's higher with variable sizes
      maxSubgroupSize = RDCMAX(maxSubgroupSize, subSizeProps.maxSubgroupSize);
    }
  }
  else if(m_pDriver->GetExtensions(NULL).ext_EXT_shader_subgroup_ballot)
  {
    // the ballot extension only proides the subgroup size on the GPU so we need to allocate worst case up front

    RDCWARN("Subgroup ballot extension is best extension enabled - using worst case subgroup size");

    maxSubgroupSize = 128;
    subgroupCapability = SubgroupCapability::EXTBallot;
  }
  else if(m_pDriver->GetExtensions(NULL).ext_EXT_shader_subgroup_vote)
  {
    // if only the vote extension is enabled we have no way to determine the subgroup size or
    // anything, so we just fall back to treating this as a degenerate case with a single thread

    RDCWARN("Subgroup vote extension is only subgroup feature enabled - treating as degenerate");

    maxSubgroupSize = 1;
    subgroupCapability = SubgroupCapability::None;
  }
}

VkSpecializationInfo VulkanReplay::MakeSpecInfo(SpecData &specData, VkSpecializationMapEntry *specMaps)
{
  memcpy(specMaps, specMapsTemplate, sizeof(specMapsTemplate));

  specMaps[(uint32_t)InputSpecConstant::Address].size =
      (m_StorageMode == BufferStorageMode::KHR_bda32 ? sizeof(uint32_t) : sizeof(uint64_t));

  VkSpecializationInfo ret = {};
  ret.dataSize = sizeof(specData);
  ret.pData = &specData;
  ret.mapEntryCount = (uint32_t)InputSpecConstant::Count;
  ret.pMapEntries = specMaps;
  return ret;
}

ShaderDebugTrace *VulkanReplay::DebugVertex(uint32_t eventId, uint32_t vertid, uint32_t instid,
                                            uint32_t idx, uint32_t view)
{
  const VulkanRenderState &state = m_pDriver->GetRenderState();
  VulkanCreationInfo &c = m_pDriver->m_CreationInfo;

  rdcstr regionName =
      StringFormat::Fmt("DebugVertex @ %u of (%u,%u,%u,%u)", eventId, vertid, instid, idx, view);

  VkMarkerRegion region(regionName);

  if(Vulkan_Debug_ShaderDebugLogging())
    RDCLOG("%s", regionName.c_str());

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

  if(!(action->flags & ActionFlags::Drawcall))
  {
    RDCLOG("No drawcall selected");
    return new ShaderDebugTrace();
  }

  uint32_t vertOffset = 0, instOffset = 0;
  if(!(action->flags & ActionFlags::Indexed))
    vertOffset = action->vertexOffset;

  if(action->flags & ActionFlags::Instanced)
    instOffset = action->instanceOffset;

  // get ourselves in pristine state before this action (without any side effects it may have had)
  m_pDriver->ReplayLog(0, eventId, eReplay_WithoutDraw);

  const VulkanCreationInfo::Pipeline &pipe = c.m_Pipeline[state.graphics.pipeline];
  const VulkanCreationInfo::ShaderEntry &shaderEntry =
      state.graphics.shaderObject ? c.m_ShaderObject[state.shaderObjects[0]].shad : pipe.shaders[0];
  VulkanCreationInfo::ShaderModule &shader = c.m_ShaderModule[shaderEntry.module];
  rdcstr entryPoint = shaderEntry.entryPoint;
  const rdcarray<SpecConstant> &spec = shaderEntry.specialization;

  VulkanCreationInfo::ShaderModuleReflection &shadRefl =
      shader.GetReflection(ShaderStage::Vertex, entryPoint, state.graphics.pipeline);

  if(!shadRefl.refl->debugInfo.debuggable)
  {
    RDCLOG("Shader is not debuggable: %s", shadRefl.refl->debugInfo.debugStatus.c_str());
    return new ShaderDebugTrace();
  }

  if((shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup) &&
     !m_pDriver->GetDeviceEnabledFeatures().vertexPipelineStoresAndAtomics)
  {
    RDCWARN("Subgroup vertex debugging is not supported without vertex stores");
    return new ShaderDebugTrace;
  }

  shadRefl.PopulateDisassembly(shader.spirv);

  VulkanAPIWrapper *apiWrapper =
      new VulkanAPIWrapper(m_pDriver, c, ShaderStage::Vertex, eventId, shadRefl.refl->resourceId);

  // clamp the view index to the number of multiviews, just to be sure
  size_t numViews;

  if(state.dynamicRendering.active)
    numViews = Log2Ceil(state.dynamicRendering.viewMask + 1);
  else
    numViews = c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].multiviews.size();
  if(numViews > 1)
    view = RDCMIN((uint32_t)numViews - 1, view);
  else
    view = 0;

  SubgroupCapability subgroupCapability = SubgroupCapability::None;
  uint32_t maxSubgroupSize = 1;
  CalculateSubgroupProperties(maxSubgroupSize, subgroupCapability);

  uint32_t numThreads = 1;

  if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
    numThreads = RDCMAX(numThreads, maxSubgroupSize);

  rdcarray<rdcarray<ShaderVariable>> &location_inputs = apiWrapper->GetLocationInputs();
  rdcarray<std::unordered_map<ShaderBuiltin, ShaderVariable>> &allthread_builtins =
      apiWrapper->GetThreadBuiltins();
  location_inputs.resize(numThreads);
  allthread_builtins.resize(numThreads);
  apiWrapper->thread_props.resize(numThreads);

  apiWrapper->thread_props[0][(size_t)rdcspv::ThreadProperty::Active] = 1;

  std::unordered_map<ShaderBuiltin, ShaderVariable> &global_builtins =
      apiWrapper->GetGlobalBuiltins();
  global_builtins[ShaderBuiltin::BaseInstance] =
      ShaderVariable(rdcstr(), action->instanceOffset, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::BaseVertex] = ShaderVariable(
      rdcstr(), (action->flags & ActionFlags::Indexed) ? action->baseVertex : action->vertexOffset,
      0U, 0U, 0U);
  global_builtins[ShaderBuiltin::DeviceIndex] = ShaderVariable(rdcstr(), 0U, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::DrawIndex] = ShaderVariable(rdcstr(), action->drawIndex, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::ViewportIndex] = ShaderVariable(rdcstr(), view, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::MultiViewIndex] = ShaderVariable(rdcstr(), view, 0U, 0U, 0U);

  bool useViewIndex = (view == ~0U) ? false : true;
  if(useViewIndex)
  {
    ResourceId rp = state.GetRenderPass();
    if(rp != ResourceId())
    {
      const VulkanCreationInfo::RenderPass &rpInfo =
          m_pDriver->GetDebugManager()->GetRenderPassInfo(rp);
      for(auto it = rpInfo.subpasses.begin(); it != rpInfo.subpasses.end(); ++it)
      {
        if(it->multiviews.isEmpty())
        {
          if(Vulkan_Debug_ShaderDebugLogging())
            RDCLOG(
                "Disabling useViewIndex because at least one subpass does not have multiple views");
          useViewIndex = false;
          break;
        }
      }
    }
    else
    {
      useViewIndex =
          (state.dynamicRendering.active ? state.dynamicRendering.viewMask : pipe.viewMask) != 0;
      if(!useViewIndex && Vulkan_Debug_ShaderDebugLogging())
        RDCLOG("Disabling useViewIndex because viewMask is zero");
    }
  }
  else
  {
    if(Vulkan_Debug_ShaderDebugLogging())
      RDCLOG("Disabling useViewIndex from input view %u", view);
  }

  // if we need to fetch subgroup data, do that now.
  uint32_t laneIndex = 0;
  if(numThreads > 1)
  {
    SpecData specData = {};

    if(action->flags & ActionFlags::Indexed)
      specData.destVertex = idx;
    else
      specData.destVertex = vertid + vertOffset;
    specData.destInstance = instid + instOffset;
    specData.destView = view == ~0U ? 0 : view;

    uint32_t paramAlign, structStride;
    rdctie(paramAlign, structStride) = GetAlignAndOutputSize(shadRefl);

    uint32_t maxHits = 4;    // we should only ever get one hit

    // struct size is rdcspv::ResultDataBase header plus Nx structStride for the number of threads
    uint32_t structSize = sizeof(rdcspv::ResultDataBase) + structStride * numThreads;

    VkDeviceSize feedbackStorageSize = maxHits * structSize + 1024;

    if(Vulkan_Debug_ShaderDebugLogging())
    {
      RDCLOG("Output structure is %u sized, output buffer is %llu bytes", structStride,
             feedbackStorageSize);
    }

    m_PatchedShaderFeedback.ResizeFeedbackBuffer(m_pDriver, feedbackStorageSize);

    specData.arrayLength = maxHits;

    // make copy of state to draw from
    VulkanRenderState modifiedstate = state;

    RDCCOMPILE_ASSERT(NumReservedBindings == 1, "NumReservedBindings is wrong");
    AddedDescriptorData patchedBufferdata = PrepareExtraBufferDescriptor(
        modifiedstate, false, {MakeNewBinding(VK_SHADER_STAGE_VERTEX_BIT)}, false);

    if(patchedBufferdata.empty())
    {
      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = ShaderStage::Vertex;

      return ret;
    }

    if(!patchedBufferdata.descSets.empty())
      m_PatchedShaderFeedback.FeedbackBuffer.WriteDescriptor(Unwrap(patchedBufferdata.descSets[0]),
                                                             0, 0);

    specData.bufferAddress = m_PatchedShaderFeedback.FeedbackBuffer.Address();
    if(specData.bufferAddress && Vulkan_Debug_ShaderDebugLogging())
    {
      RDCLOG("Got buffer address of %llu", specData.bufferAddress);
    }

    // create shader with modified code

    VkSpecializationMapEntry specMaps[(size_t)InputSpecConstant::Count];
    RDCCOMPILE_ASSERT(sizeof(specMaps) == sizeof(specMapsTemplate),
                      "Specialisation maps have changed");

    VkSpecializationInfo patchedSpecInfo = MakeSpecInfo(specData, specMaps);

    auto patchCallback = [this, &spec, &shadRefl, &patchedSpecInfo, useViewIndex, subgroupCapability,
                          maxSubgroupSize](const AddedDescriptorData &patchedBufferdata,
                                           VkShaderStageFlagBits stage, const char *entryName,
                                           const rdcarray<uint32_t> &origSpirv,
                                           rdcarray<uint32_t> &modSpirv,
                                           const VkSpecializationInfo *&specInfo) {
      if(stage != VK_SHADER_STAGE_VERTEX_BIT)
        return false;

      modSpirv = origSpirv;

      if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
        FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/debug_vsinput_before.spv", modSpirv);

      CreateInputFetcher(modSpirv, spec, shadRefl, m_StorageMode, false, false, useViewIndex,
                         subgroupCapability, maxSubgroupSize);

      if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
        FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/debug_vsinput_after.spv", modSpirv);

      // overwrite user's specialisation info. We flattened the user's spec constants when patching the SPIR-V above.
      specInfo = &patchedSpecInfo;

      return true;
    };

    PrepareStateForPatchedShader(patchedBufferdata, modifiedstate, false, patchCallback);

    if(!RunFeedbackAction(feedbackStorageSize, action, modifiedstate))
    {
      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = ShaderStage::Vertex;

      return ret;
    }

    bytebuf data;
    GetDebugManager()->GetBufferData(m_PatchedShaderFeedback.FeedbackBuffer, 0, 0, data);

    byte *base = data.data();
    uint32_t hit_count = ((uint32_t *)base)[0];
    // uint32_t total_count = ((uint32_t *)base)[1];

    RDCASSERTMSG("Should only get one hit for vertex shaders", hit_count == 1, hit_count);

    base += sizeof(Vec4f);

    rdcspv::ResultDataBase *winner = (rdcspv::ResultDataBase *)base;

    if(winner->valid != validMagicNumber)
    {
      RDCWARN("Hit doesn't have valid magic number");

      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = ShaderStage::Vertex;

      return ret;
    }

    rdcspv::Debugger *debugger = new rdcspv::Debugger;
    debugger->Parse(shader.spirv.GetSPIRV());

    // the per-thread data immediately follows the rdcspv::ResultDataBase header. Every piece of
    // data is uniformly aligned, either 16-byte by default or 32-byte if larger components exist.
    // The output is in input signature order.
    byte *LaneData = (byte *)(winner + 1);

    numThreads = 4;

    if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
    {
      RDCASSERTNOTEQUAL(winner->subgroupSize, 0);
      numThreads = RDCMAX(numThreads, winner->subgroupSize);
    }

    location_inputs.resize(numThreads);
    allthread_builtins.resize(numThreads);
    apiWrapper->thread_props.resize(numThreads);

    for(uint32_t t = 0; t < numThreads; t++)
    {
      byte *value = LaneData + t * structStride;

      {
        rdcspv::SubgroupLaneData *subgroupData = (rdcspv::SubgroupLaneData *)value;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Active] = subgroupData->isActive;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Elected] = subgroupData->elect;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::SubgroupId] = t;

        value += sizeof(rdcspv::SubgroupLaneData);
      }

      // read VertexLaneData
      {
        rdcspv::VertexLaneData *vertData = (rdcspv::VertexLaneData *)value;

        allthread_builtins[t][ShaderBuiltin::InstanceIndex] =
            ShaderVariable("InstanceIndex"_lit, vertData->inst, 0U, 0U, 0U);
        allthread_builtins[t][ShaderBuiltin::VertexIndex] =
            ShaderVariable("VertexIndex"_lit, vertData->vert, 0U, 0U, 0U);
        allthread_builtins[t][ShaderBuiltin::MultiViewIndex] =
            ShaderVariable("VertexIndex"_lit, vertData->view, 0U, 0U, 0U);

        if(view != ~0U)
          RDCASSERTEQUAL(vertData->view, view);
      }
      value += sizeof(rdcspv::VertexLaneData);

      for(size_t i = 0; i < shadRefl.refl->inputSignature.size(); i++)
      {
        const SigParameter &param = shadRefl.refl->inputSignature[i];

        bool builtin = true;
        if(param.systemValue == ShaderBuiltin::Undefined)
        {
          builtin = false;
          location_inputs[t].resize(RDCMAX((uint32_t)location_inputs.size(), param.regIndex + 1));
        }

        ShaderVariable &var =
            builtin ? allthread_builtins[t][param.systemValue] : location_inputs[t][param.regIndex];

        var.rows = 1;
        var.columns = param.compCount & 0xff;
        var.type = param.varType;

        const uint32_t comp = Bits::CountTrailingZeroes(uint32_t(param.regChannelMask));
        const uint32_t elemSize = VarTypeByteSize(param.varType);

        const size_t sz = elemSize * param.compCount;

        memcpy((var.value.u8v.data()) + elemSize * comp, value + i * paramAlign, sz);
      }
    }

    global_builtins[ShaderBuiltin::SubgroupSize] = ShaderVariable(rdcstr(), numThreads, 0U, 0U, 0U);
    apiWrapper->SetInputVarsToReadOnly();
    ShaderDebugTrace *ret = debugger->BeginDebug(apiWrapper, ShaderStage::Vertex, entryPoint, spec,
                                                 shadRefl.instructionLines, shadRefl.patchData,
                                                 winner->laneIndex, numThreads, numThreads);
    apiWrapper->ResetReplay();

    return ret;
  }
  else
  {
    // otherwise we can do a simple manual fetch of vertex inputs
    laneIndex = 0;

    std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins =
        allthread_builtins[laneIndex];
    if(action->flags & ActionFlags::Indexed)
      thread_builtins[ShaderBuiltin::VertexIndex] = ShaderVariable(rdcstr(), idx, 0U, 0U, 0U);
    else
      thread_builtins[ShaderBuiltin::VertexIndex] =
          ShaderVariable(rdcstr(), vertid + vertOffset, 0U, 0U, 0U);
    thread_builtins[ShaderBuiltin::InstanceIndex] =
        ShaderVariable(rdcstr(), instid + instOffset, 0U, 0U, 0U);

    rdcarray<ShaderVariable> &locations = location_inputs[laneIndex];
    for(const VkVertexInputAttributeDescription2EXT &attr : state.vertexAttributes)
    {
      locations.resize_for_index(attr.location);

      if(Vulkan_Debug_ShaderDebugLogging())
        RDCLOG("Populating location %u", attr.location);

      ShaderVariable &var = locations[attr.location];

      bytebuf data;

      size_t size = (size_t)GetByteSize(1, 1, 1, attr.format, 0);

      bool found = false;

      for(const VkVertexInputBindingDescription2EXT &bind : state.vertexBindings)
      {
        if(bind.binding != attr.binding)
          continue;

        if(bind.binding < state.vbuffers.size())
        {
          const VulkanRenderState::VertBuffer &vb = state.vbuffers[bind.binding];

          if(vb.buf != ResourceId())
          {
            VkDeviceSize vertexOffset = 0;

            found = true;

            if(bind.inputRate == VK_VERTEX_INPUT_RATE_INSTANCE)
            {
              if(bind.divisor == 0)
                vertexOffset = instOffset * vb.stride;
              else
                vertexOffset = (instOffset + (instid / bind.divisor)) * vb.stride;
            }
            else
            {
              vertexOffset = (idx + vertOffset) * vb.stride;
            }

            if(Vulkan_Debug_ShaderDebugLogging())
            {
              RDCLOG("Fetching from %s at %llu offset %zu bytes", ToStr(vb.buf).c_str(),
                     vb.offs + attr.offset + vertexOffset, size);
            }

            if(attr.offset + vertexOffset < vb.size)
              GetDebugManager()->GetBufferData(vb.buf, vb.offs + attr.offset + vertexOffset, size,
                                               data);
          }
        }
        else if(Vulkan_Debug_ShaderDebugLogging())
        {
          RDCLOG("Vertex binding %u out of bounds from %zu vertex buffers", bind.binding,
                 state.vbuffers.size());
        }
      }

      if(!found)
      {
        if(Vulkan_Debug_ShaderDebugLogging())
        {
          RDCLOG("Attribute binding %u out of bounds from %zu bindings", attr.binding,
                 pipe.vertexBindings.size());
        }
      }

      if(size > data.size())
      {
        // out of bounds read
        m_pDriver->AddDebugMessage(
            MessageCategory::Execution, MessageSeverity::Medium, MessageSource::RuntimeWarning,
            StringFormat::Fmt(
                "Attribute location %u from binding %u reads out of bounds at vertex %u "
                "(index %u) in instance %u.",
                attr.location, attr.binding, vertid, idx, instid));

        if(IsUIntFormat(attr.format) || IsSIntFormat(attr.format))
          var.type = VarType::UInt;
        else
          var.type = VarType::Float;

        set0001(var);
      }
      else
      {
        ResourceFormat fmt = MakeResourceFormat(attr.format);

        // integer formats need to be read as-is, rather than converted to floats
        if(fmt.compType == CompType::UInt || fmt.compType == CompType::SInt)
        {
          if(fmt.type == ResourceFormatType::R10G10B10A2)
          {
            // this is the only packed UINT format
            Vec4u decoded = ConvertFromR10G10B10A2UInt(*(uint32_t *)data.data());

            var.type = VarType::UInt;

            setUintComp(var, 0, decoded.x);
            setUintComp(var, 1, decoded.y);
            setUintComp(var, 2, decoded.z);
            setUintComp(var, 3, decoded.w);
          }
          else
          {
            var.type = VarType::UInt;

            if(fmt.compType == CompType::UInt)
            {
              if(fmt.compByteWidth == 1)
                var.type = VarType::UByte;
              else if(fmt.compByteWidth == 2)
                var.type = VarType::UShort;
              else if(fmt.compByteWidth == 4)
                var.type = VarType::UInt;
              else if(fmt.compByteWidth == 8)
                var.type = VarType::ULong;
            }
            else if(fmt.compType == CompType::SInt)
            {
              if(fmt.compByteWidth == 1)
                var.type = VarType::SByte;
              else if(fmt.compByteWidth == 2)
                var.type = VarType::SShort;
              else if(fmt.compByteWidth == 4)
                var.type = VarType::SInt;
              else if(fmt.compByteWidth == 8)
                var.type = VarType::SLong;
            }

            RDCASSERTEQUAL(fmt.compByteWidth, VarTypeByteSize(var.type));
            memcpy(var.value.u8v.data(), data.data(), fmt.compByteWidth * fmt.compCount);
          }
        }
        else
        {
          FloatVector decoded = DecodeFormattedComponents(fmt, data.data());

          var.type = VarType::Float;

          setFloatComp(var, 0, decoded.x);
          setFloatComp(var, 1, decoded.y);
          setFloatComp(var, 2, decoded.z);
          setFloatComp(var, 3, decoded.w);
        }
      }
    }

    rdcspv::Debugger *debugger = new rdcspv::Debugger;
    debugger->Parse(shader.spirv.GetSPIRV());

    global_builtins[ShaderBuiltin::SubgroupSize] = ShaderVariable(rdcstr(), numThreads, 0U, 0U, 0U);

    apiWrapper->SetInputVarsToReadOnly();
    ShaderDebugTrace *ret = debugger->BeginDebug(apiWrapper, ShaderStage::Vertex, entryPoint, spec,
                                                 shadRefl.instructionLines, shadRefl.patchData,
                                                 laneIndex, numThreads, numThreads);
    apiWrapper->ResetReplay();

    return ret;
  }
}

ShaderDebugTrace *VulkanReplay::DebugPixel(uint32_t eventId, uint32_t x, uint32_t y,
                                           const DebugPixelInputs &inputs)
{
  if(!m_pDriver->GetDeviceEnabledFeatures().fragmentStoresAndAtomics)
  {
    RDCWARN("Pixel debugging is not supported without fragment stores");
    return new ShaderDebugTrace;
  }

  uint32_t sample = inputs.sample;
  uint32_t primitive = inputs.primitive;
  uint32_t view = inputs.view;

  const VulkanRenderState &state = m_pDriver->GetRenderState();
  VulkanCreationInfo &c = m_pDriver->m_CreationInfo;

  rdcstr regionName = StringFormat::Fmt("DebugPixel @ %u of (%u,%u) sample %u primitive %u view %u",
                                        eventId, x, y, sample, primitive, view);

  VkMarkerRegion region(regionName);

  if(Vulkan_Debug_ShaderDebugLogging())
    RDCLOG("%s", regionName.c_str());

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

  if(!(action->flags & (ActionFlags::MeshDispatch | ActionFlags::Drawcall)))
  {
    RDCLOG("No drawcall selected");
    return new ShaderDebugTrace();
  }

  const VulkanCreationInfo::Pipeline &pipe = c.m_Pipeline[state.graphics.pipeline];
  ResourceId fragId = state.graphics.shaderObject ? state.shaderObjects[4] : pipe.shaders[4].module;

  if(fragId == ResourceId())
  {
    RDCLOG("No pixel shader bound at draw");
    return new ShaderDebugTrace();
  }

  // get ourselves in pristine state before this action (without any side effects it may have had)
  m_pDriver->ReplayLog(0, eventId, eReplay_WithoutDraw);

  const VulkanCreationInfo::ShaderEntry &fragEntry =
      state.graphics.shaderObject ? c.m_ShaderObject[state.shaderObjects[4]].shad : pipe.shaders[4];
  VulkanCreationInfo::ShaderModule &shader = c.m_ShaderModule[fragEntry.module];
  rdcstr entryPoint = fragEntry.entryPoint;
  const rdcarray<SpecConstant> &spec = fragEntry.specialization;

  VulkanCreationInfo::ShaderModuleReflection &shadRefl =
      shader.GetReflection(ShaderStage::Pixel, entryPoint, state.graphics.pipeline);

  if(!shadRefl.refl->debugInfo.debuggable)
  {
    RDCLOG("Shader is not debuggable: %s", shadRefl.refl->debugInfo.debugStatus.c_str());
    return new ShaderDebugTrace();
  }

  shadRefl.PopulateDisassembly(shader.spirv);

  VulkanAPIWrapper *apiWrapper =
      new VulkanAPIWrapper(m_pDriver, c, ShaderStage::Pixel, eventId, shadRefl.refl->resourceId);

  SubgroupCapability subgroupCapability = SubgroupCapability::None;
  uint32_t maxSubgroupSize = 1;
  CalculateSubgroupProperties(maxSubgroupSize, subgroupCapability);

  uint32_t numThreads = 4;

  if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
    numThreads = RDCMAX(numThreads, maxSubgroupSize);

  std::unordered_map<ShaderBuiltin, ShaderVariable> &global_builtins =
      apiWrapper->GetGlobalBuiltins();
  global_builtins[ShaderBuiltin::DeviceIndex] = ShaderVariable(rdcstr(), 0U, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::DrawIndex] = ShaderVariable(rdcstr(), action->drawIndex, 0U, 0U, 0U);

  // If the pipe contains a geometry shader, then Primitive ID cannot be used in the pixel
  // shader without being emitted from the geometry shader. For now, check if this semantic
  // will succeed in a new pixel shader with the rest of the pipe unchanged
  bool usePrimitiveID = false;

  ShaderStage prevStage = ShaderStage::Geometry;

  ResourceId prevId = state.graphics.shaderObject ? state.shaderObjects[(size_t)prevStage]
                                                  : pipe.shaders[(size_t)prevStage].module;

  if(prevId == ResourceId())
  {
    prevStage = ShaderStage::Mesh;
    prevId = state.graphics.shaderObject ? state.shaderObjects[(size_t)prevStage]
                                         : pipe.shaders[(size_t)prevStage].module;
  }

  if(prevId != ResourceId())
  {
    const VulkanCreationInfo::ShaderEntry &prevEntry =
        state.graphics.shaderObject ? c.m_ShaderObject[state.shaderObjects[(size_t)prevStage]].shad
                                    : pipe.shaders[(size_t)prevStage];
    VulkanCreationInfo::ShaderModuleReflection &prevRefl =
        c.m_ShaderModule[prevEntry.module].GetReflection(prevStage, prevEntry.entryPoint,
                                                         state.graphics.pipeline);

    // check to see if the shader outputs a primitive ID
    for(const SigParameter &e : prevRefl.refl->outputSignature)
    {
      if(e.systemValue == ShaderBuiltin::PrimitiveIndex)
      {
        if(Vulkan_Debug_ShaderDebugLogging())
        {
          RDCLOG("Geometry/mesh shader exports primitive ID, can use");
        }

        usePrimitiveID = true;
        break;
      }
    }

    if(Vulkan_Debug_ShaderDebugLogging())
    {
      if(!usePrimitiveID)
        RDCLOG("Geometry/mesh shader doesn't export primitive ID, can't use");
    }
  }
  else
  {
    // no geometry shader - safe to use as long as the geometry shader capability is available
    usePrimitiveID = m_pDriver->GetDeviceEnabledFeatures().geometryShader != VK_FALSE;

    if(Vulkan_Debug_ShaderDebugLogging())
    {
      RDCLOG("usePrimitiveID is %u because of bare capability", usePrimitiveID);
    }
  }

  bool useSampleID = m_pDriver->GetDeviceEnabledFeatures().sampleRateShading != VK_FALSE;

  if(Vulkan_Debug_ShaderDebugLogging())
  {
    RDCLOG("useSampleID is %u because of bare capability", useSampleID);
  }

  bool useViewIndex = (view == ~0U) ? false : true;
  if(useViewIndex)
  {
    ResourceId rp = state.GetRenderPass();
    if(rp != ResourceId())
    {
      const VulkanCreationInfo::RenderPass &rpInfo = GetDebugManager()->GetRenderPassInfo(rp);
      for(auto it = rpInfo.subpasses.begin(); it != rpInfo.subpasses.end(); ++it)
      {
        if(it->multiviews.isEmpty())
        {
          if(Vulkan_Debug_ShaderDebugLogging())
            RDCLOG(
                "Disabling useViewIndex because at least one subpass does not have multiple views");
          useViewIndex = false;
          break;
        }
      }
    }
    else
    {
      useViewIndex =
          (state.dynamicRendering.active ? state.dynamicRendering.viewMask : pipe.viewMask) != 0;
      if(!useViewIndex && Vulkan_Debug_ShaderDebugLogging())
        RDCLOG("Disabling useViewIndex because viewMask is zero");
    }
  }
  else
  {
    if(Vulkan_Debug_ShaderDebugLogging())
      RDCLOG("Disabling useViewIndex from input view %u", view);
  }
  if(useViewIndex)
  {
    global_builtins[ShaderBuiltin::MultiViewIndex] = ShaderVariable(rdcstr(), view, 0U, 0U, 0U);
  }

  uint32_t paramAlign, structStride;
  rdctie(paramAlign, structStride) = GetAlignAndOutputSize(shadRefl);

  uint32_t overdrawLevels = 100;    // maximum number of overdraw levels

  // struct size is rdcspv::ResultDataBase header plus Nx structStride for the number of threads
  uint32_t structSize = sizeof(rdcspv::ResultDataBase) + structStride * numThreads;

  VkDeviceSize feedbackStorageSize = overdrawLevels * structSize + sizeof(Vec4f) + 1024;

  if(Vulkan_Debug_ShaderDebugLogging())
  {
    RDCLOG("Output structure is %u sized, output buffer is %llu bytes", structStride,
           feedbackStorageSize);
  }

  m_PatchedShaderFeedback.ResizeFeedbackBuffer(m_pDriver, feedbackStorageSize);

  SpecData specData = {};

  specData.arrayLength = overdrawLevels;
  specData.destX = float(x) + 0.5f;
  specData.destY = float(y) + 0.5f;

  // make copy of state to draw from
  VulkanRenderState modifiedstate = state;

  AddedDescriptorData patchedBufferdata = PrepareExtraBufferDescriptor(
      modifiedstate, false, {MakeNewBinding(VK_SHADER_STAGE_FRAGMENT_BIT)}, false);

  if(patchedBufferdata.empty())
  {
    delete apiWrapper;

    ShaderDebugTrace *ret = new ShaderDebugTrace;
    ret->stage = ShaderStage::Pixel;

    return ret;
  }

  if(!patchedBufferdata.descSets.empty())
    m_PatchedShaderFeedback.FeedbackBuffer.WriteDescriptor(Unwrap(patchedBufferdata.descSets[0]), 0,
                                                           0);

  specData.bufferAddress = m_PatchedShaderFeedback.FeedbackBuffer.Address();
  if(specData.bufferAddress && Vulkan_Debug_ShaderDebugLogging())
  {
    RDCLOG("Got buffer address of %llu", specData.bufferAddress);
  }

  // create  shader with modified code

  VkSpecializationMapEntry specMaps[(size_t)InputSpecConstant::Count];
  RDCCOMPILE_ASSERT(sizeof(specMaps) == sizeof(specMapsTemplate),
                    "Specialisation maps have changed");

  VkSpecializationInfo patchedSpecInfo = MakeSpecInfo(specData, specMaps);

  auto patchCallback = [this, &spec, &shadRefl, &patchedSpecInfo, usePrimitiveID, useSampleID,
                        useViewIndex, subgroupCapability, maxSubgroupSize](
                           const AddedDescriptorData &patchedBufferdata, VkShaderStageFlagBits stage,
                           const char *entryName, const rdcarray<uint32_t> &origSpirv,
                           rdcarray<uint32_t> &modSpirv, const VkSpecializationInfo *&specInfo) {
    if(stage != VK_SHADER_STAGE_FRAGMENT_BIT)
      return false;

    modSpirv = origSpirv;

    if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
      FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/debug_psinput_before.spv", modSpirv);

    CreateInputFetcher(modSpirv, spec, shadRefl, m_StorageMode, usePrimitiveID, useSampleID,
                       useViewIndex, subgroupCapability, maxSubgroupSize);

    if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
      FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/debug_psinput_after.spv", modSpirv);

    // overwrite user's specialisation info. We flattened the user's spec constants when patching the SPIR-V above.
    specInfo = &patchedSpecInfo;

    return true;
  };

  PrepareStateForPatchedShader(patchedBufferdata, modifiedstate, false, patchCallback);

  if(!RunFeedbackAction(feedbackStorageSize, action, modifiedstate))
  {
    delete apiWrapper;

    ShaderDebugTrace *ret = new ShaderDebugTrace;
    ret->stage = ShaderStage::Pixel;

    return ret;
  }

  bytebuf data;
  GetDebugManager()->GetBufferData(m_PatchedShaderFeedback.FeedbackBuffer, 0, 0, data);

  byte *base = data.data();
  uint32_t hit_count = ((uint32_t *)base)[0];
  uint32_t total_count = ((uint32_t *)base)[1];

  if(hit_count > overdrawLevels)
  {
    RDCERR("%u hits, more than max overdraw levels allowed %u. Clamping", hit_count, overdrawLevels);
    hit_count = overdrawLevels;
  }

  base += sizeof(Vec4f);

  rdcspv::ResultDataBase *winner = NULL;

  RDCLOG("Got %u hit candidates out of %u total instances", hit_count, total_count);

  // if we encounter multiple hits at our destination pixel co-ord (or any other) we
  // check to see if a specific primitive was requested (via primitive parameter not
  // being set to ~0U). If it was, debug that pixel, otherwise do a best-estimate
  // of which fragment was the last to successfully depth test and debug that, just by
  // checking if the depth test is ordered and picking the final fragment in the series

  VkCompareOp depthOp = state.depthCompareOp;

  // depth tests disabled acts the same as always compare mode
  if(!state.depthTestEnable)
    depthOp = VK_COMPARE_OP_ALWAYS;

  for(uint32_t i = 0; i < hit_count; i++)
  {
    rdcspv::ResultDataBase *hit = (rdcspv::ResultDataBase *)(base + structSize * i);

    if(hit->valid != validMagicNumber)
    {
      RDCWARN("Hit %u doesn't have valid magic number", i);
      continue;
    }

    if(hit->ddxDerivCheck != 1.0f)
    {
      RDCWARN("Hit %u doesn't have valid derivatives", i);
      continue;
    }

    // if we're looking for a specific view, ignore hits from the wrong view
    if(useViewIndex)
    {
      if(hit->view != view)
        continue;
    }

    // see if this hit is a closer match than the previous winner.

    // if there's no previous winner it's clearly better
    if(winner == NULL)
    {
      winner = hit;
      continue;
    }

    // if we're looking for a specific primitive
    if(primitive != ~0U)
    {
      // and this hit is a match and the winner isn't, it's better
      if(winner->prim != primitive && hit->prim == primitive)
      {
        winner = hit;
        continue;
      }

      // if the winner is a match and we're not, we can't be better so stop now
      if(winner->prim == primitive && hit->prim != primitive)
      {
        continue;
      }
    }

    // if we're looking for a particular sample, check that
    if(sample != ~0U)
    {
      if(winner->sample != sample && hit->sample == sample)
      {
        winner = hit;
        continue;
      }

      if(winner->sample == sample && hit->sample != sample)
      {
        continue;
      }
    }

    // otherwise apply depth test
    switch(depthOp)
    {
      case VK_COMPARE_OP_NEVER:
      case VK_COMPARE_OP_EQUAL:
      case VK_COMPARE_OP_NOT_EQUAL:
      case VK_COMPARE_OP_ALWAYS:
      default:
        // don't emulate equal or not equal since we don't know the reference value. Take any hit
        // (thus meaning the last hit)
        winner = hit;
        break;
      case VK_COMPARE_OP_LESS:
        if(hit->pos.z < winner->pos.z)
          winner = hit;
        break;
      case VK_COMPARE_OP_LESS_OR_EQUAL:
        if(hit->pos.z <= winner->pos.z)
          winner = hit;
        break;
      case VK_COMPARE_OP_GREATER:
        if(hit->pos.z > winner->pos.z)
          winner = hit;
        break;
      case VK_COMPARE_OP_GREATER_OR_EQUAL:
        if(hit->pos.z >= winner->pos.z)
          winner = hit;
        break;
    }
  }

  ShaderDebugTrace *ret = NULL;

  if(winner)
  {
    rdcspv::Debugger *debugger = new rdcspv::Debugger;
    debugger->Parse(shader.spirv.GetSPIRV());

    // the per-thread data immediately follows the rdcspv::ResultDataBase header. Every piece of
    // data is uniformly aligned, either 16-byte by default or 32-byte if larger components exist.
    // The output is in input signature order.
    byte *LaneData = (byte *)(winner + 1);

    numThreads = 4;

    if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
    {
      RDCASSERTNOTEQUAL(winner->subgroupSize, 0);
      numThreads = RDCMAX(numThreads, winner->subgroupSize);
    }

    rdcarray<rdcarray<ShaderVariable>> &location_inputs = apiWrapper->GetLocationInputs();
    rdcarray<std::unordered_map<ShaderBuiltin, ShaderVariable>> &allthread_builtins =
        apiWrapper->GetThreadBuiltins();
    location_inputs.resize(numThreads);
    allthread_builtins.resize(numThreads);
    apiWrapper->thread_props.resize(numThreads);

    for(uint32_t t = 0; t < numThreads; t++)
    {
      byte *value = LaneData + t * structStride;

      if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
      {
        rdcspv::SubgroupLaneData *subgroupData = (rdcspv::SubgroupLaneData *)value;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Active] = subgroupData->isActive;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Elected] = subgroupData->elect;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::SubgroupId] = t;

        value += sizeof(rdcspv::SubgroupLaneData);
      }

      // read PixelLaneData
      {
        rdcspv::PixelLaneData *pixelData = (rdcspv::PixelLaneData *)value;

        {
          ShaderVariable &var = allthread_builtins[t][ShaderBuiltin::Position];

          var.rows = 1;
          var.columns = 4;
          var.type = VarType::Float;

          memcpy(var.value.u8v.data(), &pixelData->fragCoord, sizeof(Vec4f));
        }

        {
          ShaderVariable &var = allthread_builtins[t][ShaderBuiltin::IsHelper];

          var.rows = 1;
          var.columns = 1;
          var.type = VarType::Bool;

          memcpy(var.value.u8v.data(), &pixelData->isHelper, sizeof(uint32_t));
        }

        if(numThreads == 4)
          apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Active] = 1;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::Helper] = pixelData->isHelper;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::QuadId] = pixelData->quadId;
        apiWrapper->thread_props[t][(size_t)rdcspv::ThreadProperty::QuadLane] =
            pixelData->quadLaneIndex;
      }
      value += sizeof(rdcspv::PixelLaneData);

      for(size_t i = 0; i < shadRefl.refl->inputSignature.size(); i++)
      {
        const SigParameter &param = shadRefl.refl->inputSignature[i];

        bool builtin = true;
        if(param.systemValue == ShaderBuiltin::Undefined)
        {
          builtin = false;
          location_inputs[t].resize(RDCMAX((uint32_t)location_inputs.size(), param.regIndex + 1));
        }

        ShaderVariable &var =
            builtin ? allthread_builtins[t][param.systemValue] : location_inputs[t][param.regIndex];

        var.rows = 1;
        var.columns = param.compCount & 0xff;
        var.type = param.varType;

        const uint32_t firstComp = Bits::CountTrailingZeroes(uint32_t(param.regChannelMask));
        const uint32_t elemSize = VarTypeByteSize(param.varType);

        // we always store in 32-bit types
        const size_t sz = RDCMAX(4U, elemSize) * param.compCount;

        memcpy((var.value.u8v.data()) + elemSize * firstComp, value + i * paramAlign, sz);

        // convert down from stored 32-bit types if they were smaller
        if(elemSize == 1)
        {
          ShaderVariable tmp = var;

          for(uint32_t comp = 0; comp < param.compCount; comp++)
            var.value.u8v[comp] = tmp.value.u32v[comp] & 0xff;
        }
        else if(elemSize == 2)
        {
          ShaderVariable tmp = var;

          for(uint32_t comp = 0; comp < param.compCount; comp++)
          {
            if(VarTypeCompType(param.varType) == CompType::Float)
              var.value.f16v[comp] = rdhalf::make(tmp.value.f32v[comp]);
            else
              var.value.u16v[comp] = tmp.value.u32v[comp] & 0xffff;
          }
        }
      }
    }

    global_builtins[ShaderBuiltin::SubgroupSize] = ShaderVariable(rdcstr(), numThreads, 0U, 0U, 0U);

    apiWrapper->SetInputVarsToReadOnly();
    ret = debugger->BeginDebug(apiWrapper, ShaderStage::Pixel, entryPoint, spec,
                               shadRefl.instructionLines, shadRefl.patchData, winner->laneIndex,
                               numThreads, numThreads);
    apiWrapper->ResetReplay();
  }
  else
  {
    RDCLOG("Didn't get any valid hit to debug");
    delete apiWrapper;

    ret = new ShaderDebugTrace;
    ret->stage = ShaderStage::Pixel;
  }

  patchedBufferdata.Free();
  return ret;
}

ShaderDebugTrace *VulkanReplay::DebugThread(uint32_t eventId,
                                            const rdcfixedarray<uint32_t, 3> &groupid,
                                            const rdcfixedarray<uint32_t, 3> &threadid)
{
  return DebugComputeCommon(ShaderStage::Compute, eventId, groupid, threadid);
}

ShaderDebugTrace *VulkanReplay::DebugMeshThread(uint32_t eventId,
                                                const rdcfixedarray<uint32_t, 3> &groupid,
                                                const rdcfixedarray<uint32_t, 3> &threadid)
{
  return DebugComputeCommon(ShaderStage::Mesh, eventId, groupid, threadid);
}

ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t eventId,
                                                   const rdcfixedarray<uint32_t, 3> &groupid,
                                                   const rdcfixedarray<uint32_t, 3> &threadid)
{
  const VulkanRenderState &state = m_pDriver->GetRenderState();
  VulkanCreationInfo &c = m_pDriver->m_CreationInfo;

  rdcstr regionName =
      StringFormat::Fmt("Debug %s @ %u of (%u,%u,%u) (%u,%u,%u)", ToStr(stage).c_str(), eventId,
                        groupid[0], groupid[1], groupid[2], threadid[0], threadid[1], threadid[2]);

  VkMarkerRegion region(regionName);

  if(Vulkan_Debug_ShaderDebugLogging())
    RDCLOG("%s", regionName.c_str());

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

  if(stage == ShaderStage::Compute)
  {
    if(!(action->flags & ActionFlags::Dispatch))
    {
      RDCLOG("No dispatch selected");
      return new ShaderDebugTrace();
    }
  }
  else
  {
    if(!(action->flags & ActionFlags::MeshDispatch))
    {
      RDCLOG("No dispatch selected");
      return new ShaderDebugTrace();
    }
  }

  // get ourselves in pristine state before this dispatch (without any side effects it may have had)
  m_pDriver->ReplayLog(0, eventId, eReplay_WithoutDraw);

  const VulkanStatePipeline &stagePipeState =
      stage == ShaderStage::Compute ? state.compute : state.graphics;
  const VulkanCreationInfo::Pipeline &pipe = c.m_Pipeline[stagePipeState.pipeline];
  const VulkanCreationInfo::ShaderEntry &shaderEntry =
      stagePipeState.shaderObject ? c.m_ShaderObject[state.shaderObjects[(size_t)stage]].shad
                                  : pipe.shaders[(size_t)stage];
  VulkanCreationInfo::ShaderModule &shader = c.m_ShaderModule[shaderEntry.module];
  rdcstr entryPoint = shaderEntry.entryPoint;
  const rdcarray<SpecConstant> &spec = shaderEntry.specialization;

  VulkanCreationInfo::ShaderModuleReflection &shadRefl =
      shader.GetReflection(stage, entryPoint, stagePipeState.pipeline);

  if(!shadRefl.refl->debugInfo.debuggable)
  {
    RDCLOG("Shader is not debuggable: %s", shadRefl.refl->debugInfo.debugStatus.c_str());
    return new ShaderDebugTrace();
  }

  shadRefl.PopulateDisassembly(shader.spirv);

  VulkanAPIWrapper *apiWrapper =
      new VulkanAPIWrapper(m_pDriver, c, stage, eventId, shadRefl.refl->resourceId);

  uint32_t threadDim[3];
  threadDim[0] = shadRefl.refl->dispatchThreadsDimension[0];
  threadDim[1] = shadRefl.refl->dispatchThreadsDimension[1];
  threadDim[2] = shadRefl.refl->dispatchThreadsDimension[2];

  if((threadid[0] >= threadDim[0]) || (threadid[1] >= threadDim[1]) || (threadid[2] >= threadDim[2]))
  {
    RDCLOG("Invalid threadid %d,%d,%d selected from group %dx%dx%d", threadid[0], threadid[1],
           threadid[2], threadDim[0], threadDim[1], threadDim[2]);
    return new ShaderDebugTrace();
  }
  if((groupid[0] >= action->dispatchDimension[0]) || (groupid[1] >= action->dispatchDimension[1]) ||
     (groupid[2] >= action->dispatchDimension[2]))
  {
    RDCLOG("Invalid groupid %d,%d,%d selected from dispatch %dx%dx%d", groupid[0], groupid[1],
           groupid[2], action->dispatchDimension[0], action->dispatchDimension[1],
           action->dispatchDimension[2]);
    return new ShaderDebugTrace();
  }

  SubgroupCapability subgroupCapability = SubgroupCapability::None;
  uint32_t maxSubgroupSize = 1;
  CalculateSubgroupProperties(maxSubgroupSize, subgroupCapability);

  uint32_t numThreads = 1;

  bool hasQuadScope = (shadRefl.patchData.threadScope & rdcspv::ThreadScope::Quad) ? true : false;
  bool hasQuadDerivatives =
      (shadRefl.patchData.derivativeMode != rdcspv::ComputeDerivativeMode::None);
  bool hasSubgroupScoope =
      (shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup) ? true : false;
  bool hasWorkgroupScope =
      (shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup) ? true : false;

  if(hasQuadDerivatives || hasQuadScope)
    numThreads = RDCMAX(numThreads, 4U);
  if(hasSubgroupScoope)
    numThreads = RDCMAX(numThreads, maxSubgroupSize);
  if(hasWorkgroupScope)
    numThreads = RDCMAX(numThreads, threadDim[0] * threadDim[1] * threadDim[2]);

  rdcarray<std::unordered_map<ShaderBuiltin, ShaderVariable>> &allthread_builtins =
      apiWrapper->GetThreadBuiltins();
  allthread_builtins.resize(numThreads);
  apiWrapper->thread_props.resize(numThreads);

  std::unordered_map<ShaderBuiltin, ShaderVariable> &global_builtins =
      apiWrapper->GetGlobalBuiltins();
  global_builtins[ShaderBuiltin::DispatchSize] =
      ShaderVariable(rdcstr(), action->dispatchDimension[0], action->dispatchDimension[1],
                     action->dispatchDimension[2], 0U);
  global_builtins[ShaderBuiltin::GroupSize] =
      ShaderVariable(rdcstr(), threadDim[0], threadDim[1], threadDim[2], 0U);
  global_builtins[ShaderBuiltin::DeviceIndex] = ShaderVariable(rdcstr(), 0U, 0U, 0U, 0U);
  global_builtins[ShaderBuiltin::GroupIndex] =
      ShaderVariable(rdcstr(), groupid[0], groupid[1], groupid[2], 0U);

  const uint32_t quadIdOffset = 10000;
  const uint32_t quadDerivMode = (uint32_t)shadRefl.patchData.derivativeMode;

  uint32_t countQuadX = ~0U;
  uint32_t countQuadY = ~0U;
  uint32_t quadW = ~0U;
  uint32_t quadH = ~0U;

  if(hasQuadDerivatives)
  {
    // linear: 4x1x1
    // quad: 2x2x1
    const uint32_t quadWidths[3] = {~0U, 4, 2};
    const uint32_t quadHeights[3] = {~0U, 1, 2};
    quadW = quadWidths[quadDerivMode];
    quadH = quadHeights[quadDerivMode];
    countQuadX = threadDim[0] / quadW;
    countQuadY = threadDim[1] / quadH;
    hasQuadScope = true;
  }
  else if(hasQuadScope)
  {
    // Choose linear layout
    quadW = 4;
    quadH = 1;
    countQuadX = threadDim[0] / quadW;
    countQuadY = threadDim[1] / quadH;
  }

  if(hasQuadScope)
  {
    RDCASSERTEQUAL(threadDim[0], countQuadX * quadW);
    RDCASSERTEQUAL(threadDim[1], countQuadY * quadH);
  }

  // if we need to fetch subgroup data, do that now
  uint32_t laneIndex = 0;
  if(hasSubgroupScoope)
  {
    SpecData specData = {};

    specData.globalThreadIdX = groupid[0] * threadDim[0] + threadid[0];
    specData.globalThreadIdY = groupid[1] * threadDim[1] + threadid[1];
    specData.globalThreadIdZ = groupid[2] * threadDim[2] + threadid[2];

    uint32_t paramAlign, structStride;
    rdctie(paramAlign, structStride) = GetAlignAndOutputSize(shadRefl);

    uint32_t maxHits = 4;    // we should only ever get one hit

    // struct size is rdcspv::ResultDataBase header plus Nx structStride for the number of threads
    uint32_t structSize = sizeof(rdcspv::ResultDataBase) + structStride * maxSubgroupSize;

    VkDeviceSize feedbackStorageSize = maxHits * structSize + 1024;

    if(Vulkan_Debug_ShaderDebugLogging())
    {
      RDCLOG("Output structure is %u sized, output buffer is %llu bytes", structStride,
             feedbackStorageSize);
    }

    m_PatchedShaderFeedback.ResizeFeedbackBuffer(m_pDriver, feedbackStorageSize);

    specData.arrayLength = maxHits;

    // make copy of state to draw from
    VulkanRenderState modifiedstate = state;

    VkShaderStageFlagBits stageBit = (VkShaderStageFlagBits)ShaderMaskFromIndex(shadRefl.stageIndex);
    AddedDescriptorData patchedBufferdata = PrepareExtraBufferDescriptor(
        modifiedstate, stage == ShaderStage::Compute, {MakeNewBinding(stageBit)}, false);

    if(patchedBufferdata.empty())
    {
      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = stage;

      return ret;
    }

    if(!patchedBufferdata.descSets.empty())
      m_PatchedShaderFeedback.FeedbackBuffer.WriteDescriptor(Unwrap(patchedBufferdata.descSets[0]),
                                                             0, 0);

    specData.bufferAddress = m_PatchedShaderFeedback.FeedbackBuffer.Address();
    if(specData.bufferAddress && Vulkan_Debug_ShaderDebugLogging())
    {
      RDCLOG("Got buffer address of %llu", specData.bufferAddress);
    }

    // create shader with modified code

    VkSpecializationMapEntry specMaps[(size_t)InputSpecConstant::Count];
    RDCCOMPILE_ASSERT(sizeof(specMaps) == sizeof(specMapsTemplate),
                      "Specialisation maps have changed");

    VkSpecializationInfo patchedSpecInfo = MakeSpecInfo(specData, specMaps);

    auto patchCallback = [this, stageBit, &spec, &shadRefl, &patchedSpecInfo, subgroupCapability,
                          maxSubgroupSize](const AddedDescriptorData &patchedBufferdata,
                                           VkShaderStageFlagBits stage, const char *entryName,
                                           const rdcarray<uint32_t> &origSpirv,
                                           rdcarray<uint32_t> &modSpirv,
                                           const VkSpecializationInfo *&specInfo) {
      if(stage != stageBit)
        return false;

      modSpirv = origSpirv;

      uint32_t idx = shadRefl.stageIndex;

      static const rdcstr filename[NumShaderStages] = {
          "shadinput_vertex.spv",   "shadinput_hull.spv",  "shadinput_domain.spv",
          "shadinput_geometry.spv", "shadinput_pixel.spv", "shadinput_compute.spv",
          "shadinput_task.spv",     "shadinput_mesh.spv",
      };

      if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
        FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/before_" + filename[idx], modSpirv);

      CreateInputFetcher(modSpirv, spec, shadRefl, m_StorageMode, false, false, false,
                         subgroupCapability, maxSubgroupSize);

      if(!Vulkan_Debug_PSDebugDumpDirPath().empty())
        FileIO::WriteAll(Vulkan_Debug_PSDebugDumpDirPath() + "/after_" + filename[idx], modSpirv);

      // overwrite user's specialisation info. We flattened the user's spec constants when patching the SPIR-V above.
      specInfo = &patchedSpecInfo;

      return true;
    };

    PrepareStateForPatchedShader(patchedBufferdata, modifiedstate, stage == ShaderStage::Compute,
                                 patchCallback);

    if(!RunFeedbackAction(feedbackStorageSize, action, modifiedstate))
    {
      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = stage;

      return ret;
    }

    bytebuf data;
    GetDebugManager()->GetBufferData(m_PatchedShaderFeedback.FeedbackBuffer, 0, 0, data);

    byte *base = data.data();
    uint32_t hit_count = ((uint32_t *)base)[0];
    // uint32_t total_count = ((uint32_t *)base)[1];

    if(hit_count > maxHits)
    {
      RDCERR("%u hits, more than max overdraw levels allowed %u. Clamping", hit_count, maxHits);
      hit_count = maxHits;
    }

    base += sizeof(Vec4f);

    rdcspv::ResultDataBase *winner = (rdcspv::ResultDataBase *)base;

    if(winner->valid != validMagicNumber)
    {
      RDCWARN("Hit doesn't have valid magic number");

      delete apiWrapper;

      ShaderDebugTrace *ret = new ShaderDebugTrace;
      ret->stage = stage;

      return ret;
    }

    rdcspv::Debugger *debugger = new rdcspv::Debugger;
    debugger->Parse(shader.spirv.GetSPIRV());

    // the per-thread data immediately follows the rdcspv::ResultDataBase header. Every piece of
    // data is uniformly aligned, either 16-byte by default or 32-byte if larger components exist.
    // The output is in input signature order.
    byte *LaneData = (byte *)(winner + 1);

    const uint32_t subgroupSize = winner->subgroupSize;

    RDCASSERTNOTEQUAL(subgroupSize, 0);
    numThreads = RDCMAX(numThreads, subgroupSize);

    if(hasWorkgroupScope)
      numThreads = RDCMAX(numThreads, threadDim[0] * threadDim[1] * threadDim[2]);

    if(hasQuadScope)
      RDCASSERT(numThreads >= 4);

    global_builtins[ShaderBuiltin::NumSubgroups] =
        ShaderVariable(rdcstr(), winner->numSubgroups, 0U, 0U, 0U);

    apiWrapper->thread_props.resize(numThreads);

    laneIndex = ~0U;

    for(uint32_t t = 0; t < subgroupSize; t++)
    {
      byte *value = LaneData + t * structStride;

      rdcspv::SubgroupLaneData *subgroupData = (rdcspv::SubgroupLaneData *)value;
      value += sizeof(rdcspv::SubgroupLaneData);

      rdcspv::ComputeLaneData *compData = (rdcspv::ComputeLaneData *)value;
      value += sizeof(rdcspv::ComputeLaneData);

      uint32_t lane = t;

      uint32_t quadId = ~0U;
      uint32_t quadLaneIndex = ~0U;
      if(hasQuadScope)
      {
        uint32_t quadX = (compData->threadid[0] / quadW);
        uint32_t quadY = (compData->threadid[1] / quadH);
        uint32_t quadZ = compData->threadid[2];
        quadId = quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
        quadLaneIndex = (compData->threadid[0] % quadW) + (compData->threadid[1] % quadH) * 2;
      }

      if(hasWorkgroupScope)
      {
        if(hasQuadScope)
        {
          // There won't be any data them for inactive lanes
          if(subgroupData->isActive == 0)
            continue;

          // quad scope, derive the lane from the quad layout
          lane = quadId * 4 + quadLaneIndex;
        }
        else
        {
          // Assume linear layout for the subgroup : tightly wrapped
          lane = compData->threadid[2] * threadDim[0] * threadDim[1] +
                 compData->threadid[1] * threadDim[0] + compData->threadid[0];
        }
      }

      if(hasQuadScope)
      {
        apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
        apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId] =
            quadId + quadIdOffset;
      }

      if(rdcfixedarray<uint32_t, 3>(compData->threadid) == threadid && subgroupData->isActive)
        laneIndex = lane;

      apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Active] = subgroupData->isActive;
      apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Elected] = subgroupData->elect;
      apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::SubgroupId] = t;

      allthread_builtins[lane][ShaderBuiltin::DispatchThreadIndex] =
          ShaderVariable(rdcstr(), groupid[0] * threadDim[0] + compData->threadid[0],
                         groupid[1] * threadDim[1] + compData->threadid[1],
                         groupid[2] * threadDim[2] + compData->threadid[2], 0U);
      allthread_builtins[lane][ShaderBuiltin::GroupThreadIndex] = ShaderVariable(
          rdcstr(), compData->threadid[0], compData->threadid[1], compData->threadid[2], 0U);
      allthread_builtins[lane][ShaderBuiltin::GroupFlatIndex] =
          ShaderVariable(rdcstr(),
                         compData->threadid[2] * threadDim[0] * threadDim[1] +
                             compData->threadid[1] * threadDim[0] + compData->threadid[0],
                         0U, 0U, 0U);
      allthread_builtins[lane][ShaderBuiltin::IndexInSubgroup] =
          ShaderVariable(rdcstr(), t, 0U, 0U, 0U);
      allthread_builtins[lane][ShaderBuiltin::SubgroupIndexInWorkgroup] =
          ShaderVariable(rdcstr(), compData->subIdxInGroup, 0U, 0U, 0U);
    }

    if(laneIndex == ~0U)
    {
      RDCERR("Didn't find desired lane in subgroup data");
      laneIndex = 0;
    }

    // if we're simulating the whole workgroup we need to fill in the thread IDs of other threads
    if(hasWorkgroupScope)
    {
      for(uint32_t tz = 0; tz < threadDim[2]; tz++)
      {
        for(uint32_t ty = 0; ty < threadDim[1]; ty++)
        {
          for(uint32_t tx = 0; tx < threadDim[0]; tx++)
          {
            uint32_t quadId = ~0U;
            uint32_t quadLaneIndex = ~0U;

            uint32_t lane = ~0U;
            if(hasQuadScope)
            {
              // quad scope, derive the lane from the quad layout
              uint32_t quadX = (tx / quadW);
              uint32_t quadY = (ty / quadH);
              uint32_t quadZ = tz;
              quadId = quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
              quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;
              lane = quadId * 4 + quadLaneIndex;
            }
            else
            {
              // Assume linear layout for the subgroup : tightly wrapped
              lane = tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx;
            }
            std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins =
                allthread_builtins[lane];

            thread_builtins[ShaderBuiltin::GroupThreadIndex] =
                ShaderVariable(rdcstr(), tx, ty, tz, 0U);
            thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
                rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);

            if(apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Active])
            {
              // assert that this is the thread we expect it to be
              RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::DispatchThreadIndex].value.u32v[0],
                             groupid[0] * threadDim[0] + tx);
              RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::DispatchThreadIndex].value.u32v[1],
                             groupid[1] * threadDim[1] + ty);
              RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::DispatchThreadIndex].value.u32v[2],
                             groupid[2] * threadDim[2] + tz);

              RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::IndexInSubgroup].value.u32v[0],
                             lane % subgroupSize);
              RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::SubgroupIndexInWorkgroup].value.u32v[0],
                             lane / subgroupSize);

              if(hasQuadScope)
              {
                RDCASSERTEQUAL(
                    apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane],
                    quadLaneIndex);
                RDCASSERTEQUAL(apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId],
                               quadId + quadIdOffset);
              }
            }
            else
            {
              thread_builtins[ShaderBuiltin::DispatchThreadIndex] =
                  ShaderVariable(rdcstr(), groupid[0] * threadDim[0] + tx,
                                 groupid[1] * threadDim[1] + ty, groupid[2] * threadDim[2] + tz, 0U);
              // tightly wrap subgroups, this is likely not how the GPU actually assigns them
              thread_builtins[ShaderBuiltin::IndexInSubgroup] =
                  ShaderVariable(rdcstr(), lane % subgroupSize, 0U, 0U, 0U);
              thread_builtins[ShaderBuiltin::SubgroupIndexInWorkgroup] =
                  ShaderVariable(rdcstr(), lane / subgroupSize, 0U, 0U, 0U);
              apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Active] = 1;
              apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::SubgroupId] =
                  lane % subgroupSize;

              if(hasQuadScope)
              {
                apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane] =
                    quadLaneIndex;
                apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;
              }
            }
          }
        }
      }
    }

    // Each member of a quad should belong to the same subgroup. We assume this and do not validate it

    // Add inactive padding lanes to round up to the subgroup size
    const uint32_t numPaddingThreads = AlignUp(numThreads, subgroupSize) - numThreads;
    if(numPaddingThreads > 0)
    {
      uint32_t newNumThreads = numThreads + numPaddingThreads;
      apiWrapper->thread_props.resize(newNumThreads);
      allthread_builtins.resize(newNumThreads);
      for(uint32_t i = numThreads; i < newNumThreads; ++i)
      {
        std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins = allthread_builtins[i];

        thread_builtins[ShaderBuiltin::DispatchThreadIndex] =
            ShaderVariable(rdcstr(), -1, -1, -1, -1);
        thread_builtins[ShaderBuiltin::GroupThreadIndex] = ShaderVariable(rdcstr(), -1, -1, -1, -1);
        thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(rdcstr(), -1, -1, -1, -1);
        thread_builtins[ShaderBuiltin::IndexInSubgroup] =
            ShaderVariable(rdcstr(), i % subgroupSize, 0U, 0U, 0U);
        thread_builtins[ShaderBuiltin::SubgroupIndexInWorkgroup] =
            ShaderVariable(rdcstr(), i / subgroupSize, 0U, 0U, 0U);
        apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 0;
        apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::SubgroupId] = i % subgroupSize;
      }
      numThreads = newNumThreads;
    }
    global_builtins[ShaderBuiltin::SubgroupSize] = ShaderVariable(rdcstr(), subgroupSize, 0U, 0U, 0U);

    apiWrapper->SetInputVarsToReadOnly();
    ShaderDebugTrace *ret =
        debugger->BeginDebug(apiWrapper, stage, entryPoint, spec, shadRefl.instructionLines,
                             shadRefl.patchData, laneIndex, numThreads, subgroupSize);
    apiWrapper->ResetReplay();

    return ret;
  }
  else
  {
    // if we need to simulate the whole workgroup.
    // we assume the layout of this is irrelevant and don't attempt to read it back from the GPU
    // like we do with subgroups. We lay things out in plain linear order, along X and then Y and
    // then Z, with groups iterated together.
    if(hasWorkgroupScope)
    {
      uint32_t i = 0;
      for(uint32_t tz = 0; tz < threadDim[2]; tz++)
      {
        for(uint32_t ty = 0; ty < threadDim[1]; ty++)
        {
          for(uint32_t tx = 0; tx < threadDim[0]; tx++)
          {
            std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins =
                allthread_builtins[i];
            thread_builtins[ShaderBuiltin::DispatchThreadIndex] =
                ShaderVariable(rdcstr(), groupid[0] * threadDim[0] + tx,
                               groupid[1] * threadDim[1] + ty, groupid[2] * threadDim[2] + tz, 0U);
            thread_builtins[ShaderBuiltin::GroupThreadIndex] =
                ShaderVariable(rdcstr(), tx, ty, tz, 0U);
            thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
                rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);
            apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 1;

            if(hasQuadScope)
            {
              uint32_t quadX = (tx / quadW);
              uint32_t quadY = (ty / quadH);
              uint32_t quadZ = tz;
              uint32_t quadId =
                  quadIdOffset + quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
              uint32_t quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;

              apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
              apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;
            }

            if(rdcfixedarray<uint32_t, 3>({tx, ty, tz}) == threadid)
            {
              laneIndex = i;
            }

            i++;
          }
        }
      }
    }
    else if(hasQuadScope)
    {
      // need to simulate the whole quad, do not readback from the GPU like we do with subgroups
      // the quad is guaranteed to be in the same subgroup
      // We lay things out in linear or quad order
      RDCASSERTEQUAL(numThreads, 4U);
      uint32_t txMin = (threadid[0] / quadW) * quadW;
      uint32_t tyMin = (threadid[1] / quadH) * quadH;
      uint32_t tz = threadid[2];
      uint32_t quadZ = tz;
      for(uint32_t i = 0; i < 4U; ++i)
      {
        uint32_t tx = txMin + (i % quadW);
        uint32_t ty = tyMin + (i / quadW);
        std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins = allthread_builtins[i];
        thread_builtins[ShaderBuiltin::DispatchThreadIndex] =
            ShaderVariable(rdcstr(), groupid[0] * threadDim[0] + tx, groupid[1] * threadDim[1] + ty,
                           groupid[2] * threadDim[2] + tz, 0U);
        thread_builtins[ShaderBuiltin::GroupThreadIndex] = ShaderVariable(rdcstr(), tx, ty, tz, 0U);
        thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
            rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);
        apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 1;

        uint32_t quadX = (tx / quadW);
        uint32_t quadY = (ty / quadH);
        uint32_t quadId =
            quadIdOffset + quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
        uint32_t quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;

        apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
        apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;

        if(rdcfixedarray<uint32_t, 3>({tx, ty, tz}) == threadid)
          laneIndex = i;
      }
    }
    else
    {
      RDCASSERTEQUAL(numThreads, 1U);
      // simple single-thread case
      apiWrapper->thread_props[0][(size_t)rdcspv::ThreadProperty::Active] = 1;
      apiWrapper->thread_props[0][(size_t)rdcspv::ThreadProperty::SubgroupId] = 0;

      std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins = allthread_builtins[0];

      thread_builtins[ShaderBuiltin::DispatchThreadIndex] = ShaderVariable(
          rdcstr(), groupid[0] * threadDim[0] + threadid[0],
          groupid[1] * threadDim[1] + threadid[1], groupid[2] * threadDim[2] + threadid[2], 0U);
      thread_builtins[ShaderBuiltin::GroupThreadIndex] =
          ShaderVariable(rdcstr(), threadid[0], threadid[1], threadid[2], 0U);
      thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
          rdcstr(),
          threadid[2] * threadDim[0] * threadDim[1] + threadid[1] * threadDim[0] + threadid[0], 0U,
          0U, 0U);
    }

    rdcspv::Debugger *debugger = new rdcspv::Debugger;
    debugger->Parse(shader.spirv.GetSPIRV());

    global_builtins[ShaderBuiltin::SubgroupSize] = ShaderVariable(rdcstr(), 1U, 0U, 0U, 0U);

    apiWrapper->SetInputVarsToReadOnly();
    ShaderDebugTrace *ret =
        debugger->BeginDebug(apiWrapper, stage, entryPoint, spec, shadRefl.instructionLines,
                             shadRefl.patchData, laneIndex, numThreads, 1);
    apiWrapper->ResetReplay();

    return ret;
  }
}

rdcarray<ShaderDebugState> VulkanReplay::ContinueDebug(ShaderDebugger *debugger)
{
  rdcspv::Debugger *spvDebugger = (rdcspv::Debugger *)debugger;

  if(!spvDebugger)
    return {};

  VkMarkerRegion region("ContinueDebug Simulation Loop");

  for(size_t fmt = 0; fmt < ARRAY_COUNT(m_TexRender.DummyImageViews); fmt++)
  {
    for(size_t dim = 0; dim < ARRAY_COUNT(m_TexRender.DummyImageViews[0]); dim++)
    {
      if(m_TexRender.DummyImageViews[fmt][dim] == VK_NULL_HANDLE)
        continue;

      m_ShaderDebugData.DummyImageInfos[fmt][dim].imageLayout =
          VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
      m_ShaderDebugData.DummyImageInfos[fmt][dim].imageView =
          Unwrap(m_TexRender.DummyImageViews[fmt][dim]);

      m_ShaderDebugData.DummyWrites[fmt][dim].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
      m_ShaderDebugData.DummyWrites[fmt][dim].descriptorCount = 1;
      m_ShaderDebugData.DummyWrites[fmt][dim].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
      m_ShaderDebugData.DummyWrites[fmt][dim].dstBinding = uint32_t(dim + 1);
      m_ShaderDebugData.DummyWrites[fmt][dim].dstSet = VK_NULL_HANDLE;
      m_ShaderDebugData.DummyWrites[fmt][dim].pImageInfo =
          &m_ShaderDebugData.DummyImageInfos[fmt][dim];
    }

    m_ShaderDebugData.DummyImageInfos[fmt][5].sampler = Unwrap(m_TexRender.DummySampler);

    m_ShaderDebugData.DummyWrites[fmt][5].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    m_ShaderDebugData.DummyWrites[fmt][5].descriptorCount = 1;
    m_ShaderDebugData.DummyWrites[fmt][5].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER;
    m_ShaderDebugData.DummyWrites[fmt][5].dstBinding = (uint32_t)ShaderDebugBind::Sampler;
    m_ShaderDebugData.DummyWrites[fmt][5].dstSet = VK_NULL_HANDLE;
    m_ShaderDebugData.DummyWrites[fmt][5].pImageInfo = &m_ShaderDebugData.DummyImageInfos[fmt][5];

    if(m_TexRender.DummyBufferView[fmt] != VK_NULL_HANDLE)
    {
      m_ShaderDebugData.DummyWrites[fmt][6].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
      m_ShaderDebugData.DummyWrites[fmt][6].descriptorCount = 1;
      m_ShaderDebugData.DummyWrites[fmt][6].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
      m_ShaderDebugData.DummyWrites[fmt][6].dstBinding = (uint32_t)ShaderDebugBind::Buffer;
      m_ShaderDebugData.DummyWrites[fmt][6].dstSet = VK_NULL_HANDLE;
      m_ShaderDebugData.DummyWrites[fmt][6].pTexelBufferView =
          UnwrapPtr(m_TexRender.DummyBufferView[fmt]);
    }
  }

  rdcarray<ShaderDebugState> ret = spvDebugger->ContinueDebug();

  VulkanAPIWrapper *api = (VulkanAPIWrapper *)spvDebugger->GetAPIWrapper();
  api->ResetReplay();

  return ret;
}

void VulkanReplay::FreeDebugger(ShaderDebugger *debugger)
{
  delete debugger;
  Threading::JobSystem::SyncAllJobs();
}
