/******************************************************************************
 * 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 "spirv_debug.h"
#include "common/formatting.h"
#include "common/threading.h"
#include "core/settings.h"
#include "replay/common/var_dispatch_helpers.h"
#include "spirv_op_helpers.h"
#include "spirv_reflect.h"

RDOC_CONFIG(bool, Shader_Debug_SPIRVUseDebugColumnInformation, false,
            "Control whether column information should be read from vulkan debug info.");

RDOC_DEBUG_CONFIG(bool, Shader_Debug_UseJobSystemJobs, false,
                  "Use individual job system jobs to run shader debugging simulation.");

using namespace rdcshaders;

// this could be cleaner if ShaderVariable wasn't a very public struct, but it's not worth it so
// we just reserve value slots that we know won't be used in opaque variables.
// there's significant wasted space to keep things simple with one property = one slot
static const uint32_t OpaquePointerTypeID = 0x0dd0beef;

enum class PointerFlags
{
  RowMajorMatrix = 0x1,
  SSBO = 0x2,
  GlobalArrayBinding = 0x4,
  DereferencedPhysical = 0x8,
};

BITMASK_OPERATORS(PointerFlags);

// slot 0 for the actual pointer. Shares the same slot as the actual pointer value for GPU pointers
ShaderVariable *getPointer(ShaderVariable &var)
{
  return (ShaderVariable *)(uintptr_t)var.value.u64v[0];
}

const ShaderVariable *getPointer(const ShaderVariable &var)
{
  return (const ShaderVariable *)(uintptr_t)var.value.u64v[0];
}

void setPointer(ShaderVariable &var, const ShaderVariable *ptr)
{
  var.value.u64v[0] = (uint64_t)(uintptr_t)ptr;
}

// slot 1 is the type ID, for opaque pointers this is OpaquePointerTypeID and for real GPU pointers
// this is the type ID of the pointer. We only display this properly for base pointers -
// dereferenced pointers just show the value behind them (otherwise we'd need a pointer type for
// every child element of any pointer type, which is feasible but probably unnecessary)

// slot 2 contains the scalar indices that we carry around from dereferences
void setScalars(ShaderVariable &var, uint8_t scalar0, uint8_t scalar1)
{
  var.value.u64v[2] = (scalar0 << 8) | scalar1;
}

rdcpair<uint8_t, uint8_t> getScalars(const ShaderVariable &var)
{
  return {uint8_t((var.value.u64v[2] >> 8) & 0xff), uint8_t(var.value.u64v[2] & 0xff)};
}

// slot 3 contains the base ID of the structure, for registering pointer changes
void setBaseId(ShaderVariable &var, rdcspv::Id id)
{
  var.value.u64v[3] = id.value();
}

rdcspv::Id getBaseId(const ShaderVariable &var)
{
  return rdcspv::Id::fromWord((uint32_t)var.value.u64v[3]);
}

bool isUndefPointer(const ShaderVariable &var)
{
  return var.value.u64v[4] == 0xccccccccccccccccULL;
}

// slot 4 has the different flags we keep track of
void setPointerFlags(ShaderVariable &var, PointerFlags flags)
{
  var.value.u64v[4] = uint32_t(flags);
}

PointerFlags getPointerFlags(const ShaderVariable &var)
{
  return (PointerFlags)var.value.u64v[4];
}

void enablePointerFlags(ShaderVariable &var, PointerFlags flags)
{
  var.value.u64v[4] = uint32_t((PointerFlags)var.value.u64v[4] | flags);
}

void disablePointerFlags(ShaderVariable &var, PointerFlags flags)
{
  var.value.u64v[4] = uint32_t(PointerFlags((PointerFlags)var.value.u64v[4] & ~flags));
}

bool checkPointerFlags(const ShaderVariable &var, PointerFlags flags)
{
  return ((PointerFlags)var.value.u64v[4] & flags) == flags;
}

// slot 5 has the matrix stride
void setMatrixStride(ShaderVariable &var, uint32_t stride)
{
  var.value.u64v[5] = stride;
}

uint32_t getMatrixStride(const ShaderVariable &var)
{
  return (uint32_t)var.value.u64v[5];
}

// slot 6 has the relative byte offset. For plain bindings the global is created with an offset 0
// and then it's added to for access chains
void setByteOffset(ShaderVariable &var, uint64_t offset)
{
  var.value.u64v[6] = offset;
}

uint64_t getByteOffset(const ShaderVariable &var)
{
  return var.value.u64v[6];
}

// we also use slot 6 for the texture type (because textures and buffers requiring a byte offset are
// disjoint)
void setTextureType(ShaderVariable &var, rdcspv::DebugAPIWrapper::TextureType type)
{
  var.value.u64v[6] = type;
}

rdcspv::DebugAPIWrapper::TextureType getTextureType(const ShaderVariable &var)
{
  return (rdcspv::DebugAPIWrapper::TextureType)var.value.u64v[6];
}

// slot 7 contains the binding array index if we indexed into a global binding array
void setBindArrayIndex(ShaderVariable &var, uint32_t arrayIndex)
{
  var.value.u64v[7] = arrayIndex;
}

uint32_t getBindArrayIndex(const ShaderVariable &var)
{
  return (uint32_t)var.value.u64v[7];
}

// slot 8 contains the ID of the pointer's type, for further buffer type chasing
void setBufferTypeId(ShaderVariable &var, rdcspv::Id id)
{
  var.value.u64v[8] = id.value();
}

rdcspv::Id getBufferTypeId(const ShaderVariable &var)
{
  return rdcspv::Id::fromWord((uint32_t)var.value.u64v[8]);
}

// slot 9 is the array stride. Can't be shared with matrix stride (slot 5) in the case of matrix arrays.
void setArrayStride(ShaderVariable &var, uint32_t stride)
{
  var.value.u64v[9] = stride;
}

uint32_t getArrayStride(const ShaderVariable &var)
{
  return (uint32_t)var.value.u64v[9];
}

static ShaderVariable *pointerIfMutable(const ShaderVariable &var)
{
  return NULL;
}
static ShaderVariable *pointerIfMutable(ShaderVariable &var)
{
  return &var;
}

static uint32_t VarByteSize(const ShaderVariable &var)
{
  return VarTypeByteSize(var.type) * RDCMAX(1U, (uint32_t)var.rows) *
         RDCMAX(1U, (uint32_t)var.columns);
}

static void *VarElemPointer(ShaderVariable &var, uint32_t comp)
{
  RDCASSERTNOTEQUAL(var.type, VarType::Unknown);
  byte *ret = (byte *)var.value.u8v.data();
  return ret + comp * VarTypeByteSize(var.type);
}

static const void *VarElemPointer(const ShaderVariable &var, uint32_t comp)
{
  RDCASSERTNOTEQUAL(var.type, VarType::Unknown);
  const byte *ret = (const byte *)var.value.u8v.data();
  return ret + comp * VarTypeByteSize(var.type);
}

namespace rdcspv
{
ShaderVariable ThreadDebugBreak(ThreadState &state, uint32_t, const rdcarray<Id> &)
{
  state.DebugBreak();
  return ShaderVariable("void", 0U, 0U, 0U, 0U);
}

rdcstr GetRawName(Id id)
{
  // 32-bit value means at most 10 decimal digits, plus a preceeding _, plus trailing NULL.
  char name[12] = {};
  char *ptr = name + 10;
  uint32_t val = id.value();
  do
  {
    *ptr = char('0' + (val % 10));
    ptr--;
    val /= 10;
  } while(val);

  *ptr = '_';

  return ptr;
}

Id ParseRawName(const rdcstr &name)
{
  if(name[0] != '_')
    return Id();

  uint32_t val = 0;
  for(int i = 1; i < name.count(); i++)
  {
    if(name[i] < '0' || name[i] > '9')
      return Id();

    val *= 10;
    val += uint32_t(name[i] - '0');
  }

  return Id::fromWord(val);
}

void AssignValue(ShaderVariable &dst, const ShaderVariable &src)
{
  dst.value = src.value;

  RDCASSERTEQUAL(dst.members.size(), src.members.size());

  for(size_t i = 0; i < src.members.size(); i++)
    AssignValue(dst.members[i], src.members[i]);
}

#if ENABLED(RDOC_RELEASE)
#define CHECK_DEBUGGER_THREAD() \
  do                            \
  {                             \
  } while((void)0, 0)
#else
#define CHECK_DEBUGGER_THREAD() \
  RDCASSERTMSG("Debugger function called from non-device thread!", IsDeviceThread());
#endif    // #if ENABLED(RDOC_RELEASE)

Debugger::Debugger() : deviceThreadID(Threading::GetCurrentID())
{
}

Debugger::~Debugger()
{
  AtomicStore(&atomic_simulationFinished, 1);
  Threading::JobSystem::SyncAllJobs();
  SAFE_DELETE(apiWrapper);
}

void Debugger::ClampScalars(const ShaderVariable &var, uint8_t &scalar0) const
{
  if(scalar0 > var.columns && scalar0 != 0xff)
  {
    AddDebugMessage(MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
                    StringFormat::Fmt("Invalid scalar index %u at %u-vector %s. Clamping to %u",
                                      scalar0, var.columns, var.name.c_str(), var.columns - 1));
    scalar0 = RDCMIN((uint8_t)1, var.columns) - 1;
  }
}

void Debugger::ClampScalars(const ShaderVariable &var, uint8_t &scalar0, uint8_t &scalar1) const
{
  if(scalar0 > var.columns && scalar0 != 0xff)
  {
    AddDebugMessage(
        MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
        StringFormat::Fmt("Invalid scalar index %u at matrix %s with %u columns. Clamping to %u",
                          scalar0, var.name.c_str(), var.columns, var.columns - 1));
    scalar0 = RDCMIN((uint8_t)1, var.columns) - 1;
  }
  if(scalar1 > var.rows && scalar1 != 0xff)
  {
    AddDebugMessage(
        MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
        StringFormat::Fmt("Invalid scalar index %u at matrix %s with %u rows. Clamping to %u",
                          scalar1, var.name.c_str(), var.rows, var.rows - 1));
    scalar1 = RDCMIN((uint8_t)1, var.rows) - 1;
  }
}

void Debugger::Parse(const rdcarray<uint32_t> &spirvWords)
{
  Processor::Parse(spirvWords);
}

ConstIter Debugger::GetIterForInstruction(uint32_t inst) const
{
  return ConstIter(m_SPIRV, instructionOffsets[inst]);
}

uint32_t Debugger::GetInstructionForIter(ConstIter it) const
{
  return instructionOffsets.indexOf(it.offs());
}

uint32_t Debugger::GetInstructionForFunction(Id id) const
{
  return instructionOffsets.indexOf(functions[id].begin);
}

uint32_t Debugger::GetInstructionForLabel(Id id) const
{
  uint32_t ret = labelInstruction[id];
  RDCASSERT(ret);
  return ret;
}

const rdcspv::DataType &Debugger::GetType(Id typeId) const
{
  return dataTypes[typeId];
}

const rdcspv::DataType &Debugger::GetTypeForId(Id ssaId) const
{
  return dataTypes[idTypes[ssaId]];
}

const Decorations &Debugger::GetDecorations(Id typeId) const
{
  return decorations[typeId];
}

void Debugger::MakeSignatureNames(const rdcarray<SPIRVInterfaceAccess> &sigList,
                                  rdcarray<rdcstr> &sigNames)
{
  for(const SPIRVInterfaceAccess &sig : sigList)
  {
    rdcstr name = GetRawName(sig.ID);

    const DataType *type = &dataTypes[idTypes[sig.ID]];

    RDCASSERT(type->type == DataType::PointerType);
    type = &dataTypes[type->InnerType()];

    for(uint32_t chain : sig.accessChain)
    {
      if(type->type == DataType::ArrayType)
      {
        name += StringFormat::Fmt("[%u]", chain);
        type = &dataTypes[type->InnerType()];
      }
      else if(type->type == DataType::StructType)
      {
        if(!type->children[chain].name.empty())
          name += "." + type->children[chain].name;
        else
          name += StringFormat::Fmt("._child%u", chain);
        type = &dataTypes[type->children[chain].type];
      }
      else if(type->type == DataType::MatrixType)
      {
        name += StringFormat::Fmt(".col%u", chain);
        type = &dataTypes[type->InnerType()];
      }
      else
      {
        RDCERR("Got access chain with non-aggregate type in interface.");
        break;
      }
    }

    sigNames.push_back(name);
  }
}

// this function is implemented here to keep it next to the code we might need to update, even
// though it's checked at reflection time.
void Reflector::CheckDebuggable(bool &debuggable, rdcstr &debugStatus) const
{
  debuggable = true;
  debugStatus.clear();

  if(m_MajorVersion > 1 || m_MinorVersion > 6)
  {
    debugStatus +=
        StringFormat::Fmt("Unsupported SPIR-V version %u.%u\n", m_MajorVersion, m_MinorVersion);
    debuggable = false;
  }

  for(const Variable &g : globals)
  {
    if(g.storage == StorageClass::TaskPayloadWorkgroupEXT)
    {
      debugStatus += "Unsupported Task payload\n";
      debuggable = false;
    }
  }

  const rdcstr whitelist[] = {
      // KHR extensions
      "SPV_KHR_16bit_storage",
      "SPV_KHR_8bit_storage",
      "SPV_KHR_bit_instructions",
      "SPV_KHR_device_group",
      "SPV_KHR_expect_assume",
      "SPV_KHR_float_controls",
      "SPV_KHR_maximal_reconvergence",
      "SPV_KHR_multiview",
      "SPV_KHR_no_integer_wrap_decoration",
      "SPV_KHR_non_semantic_info",
      "SPV_KHR_physical_storage_buffer",
      "SPV_KHR_post_depth_coverage",
      "SPV_KHR_quad_control",
      "SPV_KHR_relaxed_extended_instruction",
      "SPV_KHR_shader_atomic_counter_ops",
      "SPV_KHR_shader_ballot",
      "SPV_KHR_shader_clock",
      "SPV_KHR_shader_draw_parameters",
      "SPV_KHR_storage_buffer_storage_class",
      "SPV_KHR_subgroup_rotate",
      "SPV_KHR_subgroup_uniform_control_flow",
      "SPV_KHR_subgroup_vote",
      "SPV_KHR_terminate_invocation",
      "SPV_KHR_vulkan_memory_model",
      "SPV_KHR_compute_shader_derivatives",
      "SPV_KHR_integer_dot_product",

      // EXT extensions
      "SPV_EXT_demote_to_helper_invocation",
      "SPV_EXT_descriptor_indexing",
      "SPV_EXT_fragment_fully_covered",
      "SPV_EXT_fragment_invocation_density",
      "SPV_EXT_mesh_shader",
      "SPV_EXT_physical_storage_buffer",
      "SPV_EXT_shader_atomic_float_add",
      "SPV_EXT_shader_atomic_float_min_max",
      "SPV_EXT_shader_atomic_float16_add",
      "SPV_EXT_shader_image_int64",
      "SPV_EXT_shader_stencil_export",
      "SPV_EXT_shader_viewport_index_layer",

      // vendor extensions
      "SPV_GOOGLE_decorate_string",
      "SPV_GOOGLE_hlsl_functionality1",
      "SPV_GOOGLE_user_type",
      "SPV_NV_compute_shader_derivatives",
  };

  // whitelist supported extensions
  for(const rdcstr &ext : extensions)
  {
    bool supported = false;
    for(const rdcstr &check : whitelist)
    {
      if(ext == check)
      {
        supported = true;
        break;
      }
    }

    if(supported)
      continue;

    debuggable = false;
    debugStatus += StringFormat::Fmt("Unsupported SPIR-V extension %s\n", ext.c_str());
  }

  for(Capability c : capabilities)
  {
    bool supported = false;
    switch(c)
    {
      case Capability::Matrix:
      case Capability::Shader:
      // we "support" geometry/tessellation in case the module contains other entry points, but
      // these can't be debugged right now.
      case Capability::Geometry:
      case Capability::Tessellation:
      case Capability::AtomicStorage:
      case Capability::TessellationPointSize:
      case Capability::GeometryPointSize:
      case Capability::ImageGatherExtended:
      case Capability::StorageImageMultisample:
      case Capability::UniformBufferArrayDynamicIndexing:
      case Capability::SampledImageArrayDynamicIndexing:
      case Capability::StorageBufferArrayDynamicIndexing:
      case Capability::StorageImageArrayDynamicIndexing:
      case Capability::ClipDistance:
      case Capability::CullDistance:
      case Capability::ImageCubeArray:
      case Capability::SampleRateShading:
      case Capability::ImageRect:
      case Capability::SampledRect:
      case Capability::InputAttachment:
      case Capability::MinLod:
      case Capability::Sampled1D:
      case Capability::Image1D:
      case Capability::SampledCubeArray:
      case Capability::SampledBuffer:
      case Capability::ImageBuffer:
      case Capability::ImageMSArray:
      case Capability::StorageImageExtendedFormats:
      case Capability::ImageQuery:
      case Capability::DerivativeControl:
      case Capability::TransformFeedback:
      case Capability::GeometryStreams:
      case Capability::StorageImageReadWithoutFormat:
      case Capability::StorageImageWriteWithoutFormat:
      case Capability::MultiViewport:
      case Capability::ShaderLayer:
      case Capability::ShaderViewportIndex:
      case Capability::DrawParameters:
      case Capability::DeviceGroup:
      case Capability::MultiView:
      case Capability::AtomicStorageOps:
      case Capability::SampleMaskPostDepthCoverage:
      case Capability::StencilExportEXT:
      case Capability::ShaderClockKHR:
      case Capability::ShaderViewportIndexLayerEXT:
      case Capability::FragmentFullyCoveredEXT:
      case Capability::FragmentDensityEXT:
      case Capability::ShaderNonUniform:
      case Capability::RuntimeDescriptorArray:
      case Capability::InputAttachmentArrayDynamicIndexing:
      case Capability::UniformTexelBufferArrayDynamicIndexing:
      case Capability::StorageTexelBufferArrayDynamicIndexing:
      case Capability::UniformBufferArrayNonUniformIndexing:
      case Capability::SampledImageArrayNonUniformIndexing:
      case Capability::StorageBufferArrayNonUniformIndexing:
      case Capability::StorageImageArrayNonUniformIndexing:
      case Capability::InputAttachmentArrayNonUniformIndexing:
      case Capability::UniformTexelBufferArrayNonUniformIndexing:
      case Capability::StorageTexelBufferArrayNonUniformIndexing:
      case Capability::VulkanMemoryModel:
      case Capability::VulkanMemoryModelDeviceScope:
      case Capability::DemoteToHelperInvocation:
      case Capability::AtomicFloat32AddEXT:
      case Capability::AtomicFloat32MinMaxEXT:
      case Capability::AtomicFloat16AddEXT:
      case Capability::AtomicFloat16MinMaxEXT:
      case Capability::AtomicFloat64AddEXT:
      case Capability::AtomicFloat64MinMaxEXT:
      case Capability::Float16Buffer:
      case Capability::Float16:
      case Capability::Int64:
      case Capability::Int16:
      case Capability::Int8:
      case Capability::StorageBuffer16BitAccess:
      case Capability::UniformAndStorageBuffer16BitAccess:
      case Capability::StoragePushConstant16:
      case Capability::StorageInputOutput16:
      case Capability::StorageBuffer8BitAccess:
      case Capability::UniformAndStorageBuffer8BitAccess:
      case Capability::StoragePushConstant8:
      case Capability::Float64:
      case Capability::Int64Atomics:
      case Capability::Int64ImageEXT:
      case Capability::ExpectAssumeKHR:
      case Capability::BitInstructions:
      case Capability::UniformDecoration:
      case Capability::SignedZeroInfNanPreserve:
      case Capability::PhysicalStorageBufferAddresses:
      case Capability::MeshShadingEXT:
      case Capability::QuadControlKHR:
      case Capability::GroupNonUniform:
      case Capability::GroupNonUniformArithmetic:
      case Capability::GroupNonUniformBallot:
      case Capability::GroupNonUniformClustered:
      case Capability::GroupNonUniformQuad:
      case Capability::GroupNonUniformRotateKHR:
      case Capability::GroupNonUniformShuffle:
      case Capability::GroupNonUniformShuffleRelative:
      case Capability::GroupNonUniformVote:
      case Capability::SubgroupBallotKHR:
      case Capability::SubgroupVoteKHR:
      case Capability::ComputeDerivativeGroupQuadsKHR:
      case Capability::ComputeDerivativeGroupLinearKHR:
      // SPIR-V 1.6 / SPV_KHR_integer_dot_product
      case Capability::DotProduct:
      case Capability::DotProductInput4x8Bit:
      case Capability::DotProductInput4x8BitPacked:
      case Capability::DotProductInputAll:
      {
        supported = true;
        break;
      }

      // we plan to support these but needs additional testing/proving

      // SPIR-V 1.0 MSAA custom interpolation
      case Capability::InterpolationFunction:
      {
        supported = false;
        break;
      }

      // SPIR-V 1.0 Sparse Operations
      case Capability::SparseResidency:
      {
        supported = false;
        break;
      }

      // SPIR-V 1.4 / SPV_KHR_float_controls
      case Capability::DenormPreserve:
      case Capability::DenormFlushToZero:
      case Capability::RoundingModeRTE:
      case Capability::RoundingModeRTZ:
      {
        supported = false;
        break;
      }

      // SPV_KHR_bfloat16
      case Capability::BFloat16TypeKHR:
      case Capability::BFloat16DotProductKHR:
      {
        supported = false;
        break;
      }

      // SPV_KHR_float_controls2
      case Capability::FloatControls2:
      {
        supported = false;
        break;
      }

      // SPV_KHR_fma
      case Capability::FMAKHR:
      {
        supported = false;
        break;
      }

      // SPV_KHR_fragment_shader_barycentric
      case Capability::FragmentBarycentricKHR:
      {
        supported = false;
        break;
      }

      // SPV_KHR_fragment_shading_rate
      case Capability::FragmentShadingRateKHR:
      {
        supported = false;
        break;
      }

      // SPV_KHR_untyped_pointers
      case Capability::UntypedPointersKHR:
      {
        supported = false;
        break;
      }

      // SPV_KHR_variable_pointers
      case Capability::VariablePointersStorageBuffer:
      case Capability::VariablePointers:
      {
        supported = false;
        break;
      }

      // SPV_KHR_workgroup_memory_explicit_layout
      case Capability::WorkgroupMemoryExplicitLayout16BitAccessKHR:
      case Capability::WorkgroupMemoryExplicitLayout8BitAccessKHR:
      case Capability::WorkgroupMemoryExplicitLayoutKHR:
      {
        supported = false;
        break;
      }

      // Ray tracing
      case Capability::RayCullMaskKHR:
      case Capability::RayQueryKHR:
      case Capability::RayQueryPositionFetchKHR:
      case Capability::RayTracingKHR:
      case Capability::RayTracingPositionFetchKHR:
      case Capability::RayTraversalPrimitiveCullingKHR:
      case Capability::RayTracingOpacityMicromapEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_float8
      case Capability::Float8EXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_fragment_shader_interlock
      case Capability::FragmentShaderSampleInterlockEXT:
      case Capability::FragmentShaderShadingRateInterlockEXT:
      case Capability::FragmentShaderPixelInterlockEXT:
      {
        supported = false;
        break;
      }

      case Capability::ReplicatedCompositesEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_shader_64bit_indexing
      case Capability::Shader64BitIndexingEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_shader_tile_image
      case Capability::TileImageColorReadAccessEXT:
      case Capability::TileImageDepthReadAccessEXT:
      case Capability::TileImageStencilReadAccessEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_shader_invocation_reorder
      case Capability::ShaderInvocationReorderEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_shader_subgroup_partitioned
      case Capability::GroupNonUniformPartitionedEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_long_vector
      case Capability::LongVectorEXT:
      {
        supported = false;
        break;
      }

      // SPV_EXT_descriptor_heap
      case Capability::DescriptorHeapEXT:
      {
        supported = false;
        break;
      }

      // no plans to support these - mostly Kernel/OpenCL related or vendor extensions
      case Capability::Addresses:
      case Capability::Linkage:
      case Capability::Kernel:
      case Capability::Groups:
      case Capability::Vector16:
      case Capability::ImageBasic:
      case Capability::ImageReadWrite:
      case Capability::ImageMipmap:
      case Capability::Pipes:
      case Capability::DeviceEnqueue:
      case Capability::LiteralSampler:
      case Capability::GenericPointer:
      case Capability::SubgroupDispatch:
      case Capability::NamedBarrier:
      case Capability::PipeStorage:
      case Capability::Float16ImageAMD:
      case Capability::ImageGatherBiasLodAMD:
      case Capability::FragmentMaskAMD:
      case Capability::ImageReadWriteLodAMD:
      case Capability::SampleMaskOverrideCoverageNV:
      case Capability::GeometryShaderPassthroughNV:
      case Capability::ShaderViewportMaskNV:
      case Capability::ShaderStereoViewNV:
      case Capability::PerViewAttributesNV:
      case Capability::MeshShadingNV:
      case Capability::ImageFootprintNV:
      case Capability::CooperativeMatrixNV:
      case Capability::ShaderSMBuiltinsNV:
      case Capability::SubgroupShuffleINTEL:
      case Capability::SubgroupBufferBlockIOINTEL:
      case Capability::SubgroupImageBlockIOINTEL:
      case Capability::SubgroupImageMediaBlockIOINTEL:
      case Capability::IntegerFunctions2INTEL:
      case Capability::SubgroupAvcMotionEstimationINTEL:
      case Capability::SubgroupAvcMotionEstimationIntraINTEL:
      case Capability::SubgroupAvcMotionEstimationChromaINTEL:
      case Capability::FunctionPointersINTEL:
      case Capability::IndirectReferencesINTEL:
      case Capability::FPGAKernelAttributesINTEL:
      case Capability::FPGALoopControlsALTERA:
      case Capability::FPGAMemoryAttributesALTERA:
      case Capability::FPGARegALTERA:
      case Capability::UnstructuredLoopControlsINTEL:
      case Capability::KernelAttributesINTEL:
      case Capability::BlockingPipesALTERA:
      case Capability::RayTracingMotionBlurNV:
      case Capability::RoundToInfinityINTEL:
      case Capability::FloatingPointModeINTEL:
      case Capability::AsmINTEL:
      case Capability::VectorAnyINTEL:
      case Capability::VectorComputeINTEL:
      case Capability::VariableLengthArrayINTEL:
      case Capability::FunctionFloatControlINTEL:
      case Capability::FPFastMathModeINTEL:
      case Capability::ArbitraryPrecisionFixedPointALTERA:
      case Capability::ArbitraryPrecisionFloatingPointALTERA:
      case Capability::ArbitraryPrecisionIntegersALTERA:
      case Capability::FPGAMemoryAccessesALTERA:
      case Capability::FPGAClusterAttributesALTERA:
      case Capability::LoopFuseALTERA:
      case Capability::FPGABufferLocationALTERA:
      case Capability::USMStorageClassesALTERA:
      case Capability::IOPipesALTERA:
      case Capability::LongCompositesINTEL:
      case Capability::DebugInfoModuleINTEL:
      case Capability::BindlessTextureNV:
      case Capability::MemoryAccessAliasingINTEL:
      case Capability::SplitBarrierINTEL:
      case Capability::GroupUniformArithmeticKHR:
      case Capability::CoreBuiltinsARM:
      case Capability::FPGADSPControlALTERA:
      case Capability::FPGAInvocationPipeliningAttributesALTERA:
      case Capability::RuntimeAlignedAttributeALTERA:
      case Capability::TextureSampleWeightedQCOM:
      case Capability::TextureBoxFilterQCOM:
      case Capability::TextureBlockMatchQCOM:
      case Capability::BFloat16ConversionINTEL:
      case Capability::FPGAKernelAttributesv2INTEL:
      case Capability::FPGALatencyControlALTERA:
      case Capability::FPGAArgumentInterfacesALTERA:
      case Capability::TextureBlockMatch2QCOM:
      case Capability::ShaderEnqueueAMDX:
      case Capability::DisplacementMicromapNV:
      case Capability::AtomicFloat16VectorNV:
      case Capability::RayTracingDisplacementMicromapNV:
      case Capability::CooperativeMatrixKHR:
      case Capability::CooperativeVectorNV:
      case Capability::CooperativeVectorTrainingNV:
      case Capability::CooperativeMatrixReductionsNV:
      case Capability::CooperativeMatrixConversionsNV:
      case Capability::CooperativeMatrixPerElementOperationsNV:
      case Capability::CooperativeMatrixTensorAddressingNV:
      case Capability::CooperativeMatrixBlockLoadsNV:
      case Capability::FPGAClusterAttributesV2ALTERA:
      case Capability::FPMaxErrorINTEL:
      case Capability::GlobalVariableFPGADecorationsALTERA:
      case Capability::MaskedGatherScatterINTEL:
      case Capability::CacheControlsINTEL:
      case Capability::RegisterLimitsINTEL:
      case Capability::GlobalVariableHostAccessINTEL:
      case Capability::SubgroupBufferPrefetchINTEL:
      case Capability::Subgroup2DBlockIOINTEL:
      case Capability::Subgroup2DBlockTransformINTEL:
      case Capability::Subgroup2DBlockTransposeINTEL:
      case Capability::SubgroupMatrixMultiplyAccumulateINTEL:
      case Capability::CooperativeMatrixLayoutsARM:
      case Capability::RawAccessChainsNV:
      case Capability::RayTracingSpheresGeometryNV:
      case Capability::RayTracingLinearSweptSpheresGeometryNV:
      case Capability::RayTracingClusterAccelerationStructureNV:
      case Capability::TensorAddressingNV:
      case Capability::OptNoneEXT:
      case Capability::ArithmeticFenceEXT:
      case Capability::TensorsARM:
      case Capability::StorageTensorArrayDynamicIndexingARM:
      case Capability::StorageTensorArrayNonUniformIndexingARM:
      case Capability::TileShadingQCOM:
      case Capability::Int4TypeINTEL:
      case Capability::Int4CooperativeMatrixINTEL:
      case Capability::TaskSequenceALTERA:
      case Capability::TernaryBitwiseFunctionINTEL:
      case Capability::TensorFloat32RoundingINTEL:
      case Capability::GraphARM:
      case Capability::BFloat16CooperativeMatrixKHR:
      case Capability::Float8CooperativeMatrixEXT:
      case Capability::CooperativeMatrixConversionQCOM:
      case Capability::UntypedVariableLengthArrayINTEL:
      case Capability::SpecConditionalINTEL:
      case Capability::FunctionVariantsINTEL:
      case Capability::BindlessImagesINTEL:
      case Capability::RayTracingNV:
      case Capability::ShaderInvocationReorderNV:
      case Capability::PushConstantBanksNV:
      case Capability::Max:
      case Capability::Invalid:
      {
        supported = false;
        break;
      }

      // deprecated provisional raytracing
      case Capability::RayQueryProvisionalKHR:
      case Capability::RayTracingProvisionalKHR:
      {
        supported = false;
        break;
      }
    }

    if(!supported)
    {
      debuggable = false;
      debugStatus += StringFormat::Fmt("Unsupported capability '%s'\n", ToStr(c).c_str());
    }
  }

  for(auto it = extSets.begin(); it != extSets.end(); it++)
  {
    Id id = it->first;
    const rdcstr &setname = it->second;

    if(setname == "GLSL.std.450" || setname.beginsWith("NonSemantic."))
      continue;

    debuggable = false;
    debugStatus += StringFormat::Fmt("Unsupported extended instruction set: '%s'\n", setname.c_str());
  }

  // we don't currently support debugging unbounded arrays of cbuffers
  for(const Variable &v : globals)
  {
    if(v.storage == StorageClass::Uniform)
    {
      const DataType &type = dataTypes[v.type];

      // global variables should all be pointers into opaque storage
      RDCASSERT(type.type == DataType::PointerType);

      const DataType *innertype = &dataTypes[type.InnerType()];

      if(innertype->type == DataType::ArrayType)
      {
        if(innertype->length == Id())
        {
          debuggable = false;
          rdcstr name = strings[v.id];
          if(name.empty())
            name = GetRawName(v.id);
          debugStatus +=
              StringFormat::Fmt("Unsupported unbounded uniform buffer array: '%s'\n", name.c_str());
        }
      }
    }
  }

  debugStatus.trim();
}

void Debugger::SetStructArrayNames(ShaderVariable &c, const DataType *typeWalk,
                                   const rdcarray<SpecConstant> &specInfo)
{
  if(typeWalk->type == DataType::StructType)
  {
    RDCASSERTEQUAL(c.members.size(), typeWalk->children.size());
    for(size_t i = 0; i < c.members.size(); ++i)
    {
      const DataType::Child &child = typeWalk->children[i];
      const DataType *childType = &dataTypes[child.type];
      if(!child.name.empty())
        c.members[i].name = child.name;
      else
        c.members[i].name = StringFormat::Fmt("_child%d", i);

      SetStructArrayNames(c.members[i], childType, specInfo);
    }
  }
  else if(typeWalk->type == DataType::ArrayType)
  {
    uint32_t arraySize = EvaluateConstant(typeWalk->length, specInfo).value.u32v[0];
    const DataType *childType = &dataTypes[typeWalk->InnerType()];
    for(size_t i = 0; i < arraySize; ++i)
    {
      c.members[i].name = StringFormat::Fmt("[%u]", i);
      SetStructArrayNames(c.members[i], childType, specInfo);
    }
  }
}

ShaderDebugTrace *Debugger::BeginDebug(DebugAPIWrapper *api, const ShaderStage shaderStage,
                                       const rdcstr &entryPoint,
                                       const rdcarray<SpecConstant> &specInfo,
                                       const std::map<size_t, uint32_t> &instructionLines,
                                       const SPIRVPatchData &patchData, uint32_t activeIndex,
                                       uint32_t threadsInWorkgroup, uint32_t threadsInSubgroup)
{
  Id entryId = entryLookup[ShaderEntryPoint(entryPoint, shaderStage)];

  if(entryId == Id())
  {
    RDCERR("Invalid entry point '%s'", entryPoint.c_str());
    return new ShaderDebugTrace;
  }

  rdcarray<Id> entryInterface;

  for(const EntryPoint &e : entries)
  {
    if(e.id == entryId)
    {
      entryInterface = e.usedIds;
      break;
    }
  }

  global.clock = uint64_t(time(NULL)) << 32;

  for(auto it = extSets.begin(); it != extSets.end(); it++)
  {
    Id id = it->first;
    const rdcstr &setname = it->second;

    if(setname == "GLSL.std.450")
    {
      ExtInstDispatcher extinst;

      extinst.name = setname;

      ConfigureGLSLStd450(extinst);

      global.extInsts[id] = extinst;
    }
    else if(setname == "NonSemantic.DebugBreak")
    {
      ExtInstDispatcher extinst;

      extinst.name = setname;

      // idx 0 is unused, fill with a dummy function
      extinst.names.push_back("__");
      extinst.functions.push_back(&ThreadDebugBreak);
      extinst.names.push_back("DebugBreak");
      extinst.functions.push_back(&ThreadDebugBreak);

      global.extInsts[id] = extinst;

      RDCLOG("extinst set %u is debug break", id.value());
    }
    else if(setname.beginsWith("NonSemantic."))
    {
      ExtInstDispatcher extinst;

      extinst.name = setname;

      extinst.skippedNonsemantic = true;

      global.extInsts[id] = extinst;
    }
  }

  ShaderDebugTrace *ret = new ShaderDebugTrace;
  ret->debugger = this;
  ret->stage = shaderStage;
  activeLaneIndex = activeIndex;
  subgroupSize = threadsInSubgroup;
  stage = shaderStage;
  apiWrapper = api;
  ShaderFeatures shaderFeatures = ShaderFeatures::None;
  if((stage == ShaderStage::Fragment) ||
     ((stage == ShaderStage::Compute) && patchData.derivativeMode != ComputeDerivativeMode::None))
    shaderFeatures |= ShaderFeatures::Derivatives;

  queuedDeviceThreadSteps.resize(threadsInWorkgroup);
  queuedGpuMathOps.resize(threadsInWorkgroup);
  queuedGpuSampleGatherOps.resize(threadsInWorkgroup);
  pendingLanes.resize(threadsInWorkgroup);
  queuedJobs.resize(threadsInWorkgroup);
  for(uint32_t i = 0; i < threadsInWorkgroup; i++)
  {
    workgroup.push_back(ThreadState(*this, global, stage, shaderFeatures));
    queuedDeviceThreadSteps[i] = false;
    queuedGpuMathOps[i] = false;
    queuedGpuSampleGatherOps[i] = false;
    pendingLanes[i] = false;
    queuedJobs[i] = 0;
  }

  ThreadState &active = GetActiveLane();

  active.nextInstruction = instructionOffsets.indexOf(functions[entryId].begin);

  active.ids.resize(idOffsets.size());

  // array names and struct member names are not set when constants are created
  for(auto it = constants.begin(); it != constants.end(); ++it)
  {
    Constant &c = it->second;

    const DataType *typeWalk = &dataTypes[c.type];
    SetStructArrayNames(c.value, typeWalk, specInfo);
  }

  // evaluate all constants
  for(auto it = constants.begin(); it != constants.end(); it++)
  {
    active.ids[it->first] = EvaluateConstant(it->first, specInfo);
    active.ids[it->first].name = GetRawName(it->first);
  }

  rdcarray<rdcstr> inputSigNames, outputSigNames;

  MakeSignatureNames(patchData.inputs, inputSigNames);
  MakeSignatureNames(patchData.outputs, outputSigNames);

  struct PointerId
  {
    PointerId(Id i, rdcarray<ShaderVariable> GlobalState::*th, rdcarray<ShaderVariable> &storage)
        : id(i), globalStorage(th), globalIndex(storage.size() - 1)
    {
    }
    PointerId(Id i, rdcarray<ShaderVariable> ThreadState::*th, rdcarray<ShaderVariable> &storage)
        : id(i), threadStorage(th), threadIndex(storage.size() - 1)
    {
    }
    PointerId(Id i, rdcarray<ShaderVariable> GlobalState::*global,
              rdcarray<ShaderVariable> &globalVars, rdcarray<ShaderVariable> ThreadState::*thread,
              rdcarray<ShaderVariable> &threadVars)
        : id(i),
          globalStorage(global),
          globalIndex(globalVars.size() - 1),
          threadStorage(thread),
          threadIndex(threadVars.size() - 1)
    {
    }

    void Set(Debugger &d, const GlobalState &global, ThreadState &lane, bool forceLocalGSM) const
    {
      const bool isGlobal = (globalIndex != UINT_MAX);
      const bool isGSM = isGlobal && (threadIndex != UINT_MAX);
      const bool useLocal = (forceLocalGSM && isGSM) || !isGlobal;

      if(!useLocal)
        lane.ids[id] = d.MakePointerVariable(id, &(global.*globalStorage)[globalIndex]);
      else
        lane.ids[id] = d.MakePointerVariable(id, &(lane.*threadStorage)[threadIndex]);
    }

    Id id;
    rdcarray<ShaderVariable> GlobalState::*globalStorage = NULL;
    rdcarray<ShaderVariable> ThreadState::*threadStorage = NULL;
    size_t globalIndex = UINT_MAX;
    size_t threadIndex = UINT_MAX;
  };

#define GLOBAL_POINTER(id, list) PointerId(id, &GlobalState::list, global.list)
#define THREAD_POINTER(id, list) PointerId(id, &ThreadState::list, active.list)
#define GSM_POINTER(id, globalList, threadList)                                        \
  PointerId(id, &GlobalState::globalList, global.globalList, &ThreadState::threadList, \
            active.threadList)

  rdcarray<PointerId> pointerIDs;

  // tracking for any GL bare uniforms
  uint32_t uniformsCBuffer = ~0U;
  rdcarray<rdcpair<rdcspv::Id, size_t>> bareUniformPointers;

  // allocate storage for globals with opaque storage classes, and prepare to set up pointers to
  // them for the global variables themselves
  for(const Variable &v : globals)
  {
    if(v.storage == StorageClass::Input || v.storage == StorageClass::Output)
    {
      if(!entryInterface.contains(v.id))
        continue;

      const bool isInput = (v.storage == StorageClass::Input);

      ShaderVariable var;
      var.name = GetRawName(v.id);

      rdcstr rawName = var.name;
      rdcstr sourceName = GetHumanName(v.id);

      const DataType &type = dataTypes[v.type];

      // global variables should all be pointers into opaque storage
      RDCASSERT(type.type == DataType::PointerType);

      // if we don't have a good human name, generate a better one using the interface information
      // we have
      if(sourceName == var.name)
      {
        if(decorations[v.id].flags & Decorations::HasBuiltIn)
        {
          sourceName = StringFormat::Fmt("_%s", ToStr(decorations[v.id].builtIn).c_str());
        }
        else if(decorations[v.id].flags & Decorations::HasLocation)
        {
          sourceName =
              StringFormat::Fmt("_%s%u", isInput ? "input" : "output", decorations[v.id].location);
        }
        else
        {
          sourceName = StringFormat::Fmt("_sig%u", v.id.value());

          // on GL, detect and name gl_PerVertex as the builtin struct
          if(api->GetGraphicsAPI() == GraphicsAPI::OpenGL)
          {
            if(!dataTypes[type.InnerType()].children.empty() &&
               dataTypes[type.InnerType()].children[0].decorations.flags & Decorations::HasBuiltIn)
              sourceName = "gl_PerVertex";
          }
        }

        for(const DecorationAndParamData &d : decorations[v.id].others)
        {
          if(d.value == Decoration::Component)
            sourceName += StringFormat::Fmt("_%u", d.component);
        }
      }

      const rdcarray<rdcstr> &sigNames = isInput ? inputSigNames : outputSigNames;

      bool addSource = m_DebugInfo.valid ? m_DebugInfo.globals.contains(v.id) : true;

      // modified in a loop below as needed
      uint32_t laneIndex = 0;

      // fill the interface variable
      auto fillInputCallback = [this, isInput, addSource, ret, &sigNames, &rawName, &sourceName,
                                &laneIndex](ShaderVariable &var, const Decorations &curDecorations,
                                            const DataType &type, uint64_t location,
                                            const rdcstr &accessSuffix) {
        if(!var.members.empty())
          return;

        if(isInput)
        {
          uint32_t component = 0;
          for(const DecorationAndParamData &dec : curDecorations.others)
          {
            if(dec.value == Decoration::Component)
            {
              component = dec.component;
              break;
            }
          }

          ShaderBuiltin builtin = ShaderBuiltin::Undefined;
          if(curDecorations.flags & Decorations::HasBuiltIn)
            builtin = MakeShaderBuiltin(stage, curDecorations.builtIn);

          // Only set inputs for active lanes
          if(apiWrapper->GetThreadProperty(laneIndex, ThreadProperty::Active) != 0)
            this->apiWrapper->FillInputValue(var, builtin, laneIndex, (uint32_t)location, component);
        }
        else
        {
          // make it obvious when uninitialised outputs are written
          memset(&var.value, 0xcc, sizeof(var.value));
        }

        bool addSourceVar = false;

        if(!isInput && addSource)
          addSourceVar = true;

        if(isInput && laneIndex == activeLaneIndex)
          addSourceVar = true;

        if(sourceName != rawName && addSourceVar)
        {
          rdcstr debugVarName = rawName + accessSuffix;

          SourceVariableMapping sourceVar;
          sourceVar.name = sourceName + accessSuffix;
          sourceVar.offset = (uint32_t)location;
          sourceVar.type = var.type;
          sourceVar.rows = var.rows;
          sourceVar.columns = var.columns;
          sourceVar.signatureIndex = sigNames.indexOf(debugVarName);

          StripCommonGLPrefixes(sourceVar.name);

          for(uint32_t x = 0; x < uint32_t(var.rows) * var.columns; x++)
            sourceVar.variables.push_back(DebugVariableReference(
                isInput ? DebugVariableType::Input : DebugVariableType::Variable, debugVarName, x));

          if(addSourceVar)
            ret->sourceVars.push_back(sourceVar);
        }
      };

      if(isInput)
      {
        for(laneIndex = 0; laneIndex < threadsInWorkgroup; laneIndex++)
        {
          // create the opaque storage
          workgroup[laneIndex].inputs.push_back(var);

          WalkVariable<ShaderVariable, true>(decorations[v.id], dataTypes[type.InnerType()], ~0U,
                                             false, workgroup[laneIndex].inputs.back(), rdcstr(),
                                             fillInputCallback);
        }

        // then make sure we know which ID to set up for the pointer
        pointerIDs.push_back(THREAD_POINTER(v.id, inputs));
      }
      else
      {
        WalkVariable<ShaderVariable, true>(decorations[v.id], dataTypes[type.InnerType()], ~0U,
                                           false, var, rdcstr(), fillInputCallback);

        active.outputs.push_back(var);
        liveGlobals.push_back(v.id);
        pointerIDs.push_back(THREAD_POINTER(v.id, outputs));
      }
    }

    // pick up uniform globals, which could be cbuffers, and push constants
    else if(v.storage == StorageClass::Uniform || v.storage == StorageClass::StorageBuffer ||
            v.storage == StorageClass::PushConstant)
    {
      if(!patchData.usedIds.contains(v.id))
        continue;

      ShaderVariable var;
      var.name = GetRawName(v.id);

      rdcstr sourceName = GetHumanName(v.id);

      const DataType &type = dataTypes[v.type];

      // global variables should all be pointers into opaque storage
      RDCASSERT(type.type == DataType::PointerType);

      const DataType *innertype = &dataTypes[type.InnerType()];

      if(sourceName == var.name)
        sourceName = GetHumanName(innertype->id);

      bool isArray = false;
      uint32_t arraySize = 1;
      if(innertype->type == DataType::ArrayType)
      {
        isArray = true;
        if(innertype->length == Id())
          arraySize = ~0U;
        else
          arraySize = EvaluateConstant(innertype->length, specInfo).value.u32v[0];
        innertype = &dataTypes[innertype->InnerType()];
      }

      const bool ssbo = (v.storage == StorageClass::StorageBuffer) ||
                        (decorations[innertype->id].flags & Decorations::BufferBlock);

      if(innertype->type == DataType::StructType)
      {
        // if we don't have a good human name, generate a better one using the interface information
        // we have
        if(sourceName == var.name)
        {
          if(v.storage == StorageClass::PushConstant)
            sourceName = "_pushconsts";
          else if(ssbo)
            sourceName = StringFormat::Fmt("_buffer_set%u_bind%u", decorations[v.id].set,
                                           decorations[v.id].binding);
          else
            sourceName = StringFormat::Fmt("_cbuffer_set%u_bind%u", decorations[v.id].set,
                                           decorations[v.id].binding);
        }

        SourceVariableMapping sourceVar;
        sourceVar.name = sourceName;
        sourceVar.offset = 0;

        if(ssbo)
        {
          var.rows = 1;
          var.columns = 1;
          var.type = VarType::ReadWriteResource;

          int32_t idx = patchData.rwInterface.indexOf(v.id);

          // on GL we may have buffers which are dead-code eliminated but remain part of the simulated
          // code. Because we base our interfaces off the GLSL reflected data it may not be present
          if(idx >= 0)
            var.SetBindIndex(ShaderBindIndex(DescriptorCategory::ReadWriteResource, idx, 0U));
          else
            var.SetBindIndex(ShaderBindIndex());

          if(api->GetGraphicsAPI() == GraphicsAPI::Vulkan)
            RDCASSERT(idx >= 0);

          enablePointerFlags(var, PointerFlags::SSBO);

          if(isArray)
            enablePointerFlags(var, PointerFlags::GlobalArrayBinding);

          sourceVar.type = VarType::ReadWriteResource;
          sourceVar.rows = 1;
          sourceVar.columns = 1;
          sourceVar.variables.push_back(
              DebugVariableReference(DebugVariableType::ReadWriteResource, var.name));

          global.readWriteResources.push_back(var);
          pointerIDs.push_back(GLOBAL_POINTER(v.id, readWriteResources));
        }
        else
        {
          ShaderBindIndex binding;

          binding.category = DescriptorCategory::ConstantBlock;
          binding.index = patchData.cblockInterface.indexOf(v.id);

          // on GL we may have buffers which are dead-code eliminated but remain part of the simulated
          // code. Because we base our interfaces off the GLSL reflected data it may not be present
          if(binding.index == ~0U)
            binding = ShaderBindIndex();

          if(api->GetGraphicsAPI() == GraphicsAPI::Vulkan)
            RDCASSERT(binding.index != ~0U);

          auto cbufferCallback = [this, &binding](
                                     ShaderVariable &var, const Decorations &curDecorations,
                                     const DataType &type, uint64_t offset, const rdcstr &) {
            if(!var.members.empty())
              return;

            // non-matrix case is simple, just read the size of the variable
            if(var.rows == 1)
            {
              this->apiWrapper->ReadBufferValue(binding, offset, VarByteSize(var),
                                                var.value.u8v.data());

              if(type.type == DataType::PointerType)
              {
                var.SetTypedPointer(var.value.u64v[0], this->apiWrapper->GetShaderID(),
                                    idToPointerType[type.InnerType()]);

                const Decorations &dec = decorations[type.id];
                if(dec.flags & Decorations::HasArrayStride)
                  setArrayStride(var, dec.arrayStride);
              }
            }
            else
            {
              // matrix case is more complicated. Either read column by column or row by row
              // depending on majorness
              uint32_t matrixStride = curDecorations.matrixStride;

              if(!(curDecorations.flags & Decorations::HasMatrixStride))
              {
                RDCWARN("Matrix without matrix stride - assuming legacy vec4 packed");
                matrixStride = 16;
              }

              if(curDecorations.flags & Decorations::ColMajor)
              {
                ShaderVariable tmp;
                tmp.type = var.type;

                uint32_t colSize = VarTypeByteSize(var.type) * var.rows;
                for(uint32_t c = 0; c < var.columns; c++)
                {
                  // read the column
                  this->apiWrapper->ReadBufferValue(binding, offset + c * matrixStride, colSize,
                                                    VarElemPointer(tmp, 0));

                  // now write it into the appropiate elements in the destination ShaderValue
                  for(uint32_t r = 0; r < var.rows; r++)
                    copyComp(var, r * var.columns + c, tmp, r);
                }
              }
              else
              {
                // row major is easier, read row-by-row directly into the output variable
                uint32_t rowSize = VarTypeByteSize(var.type) * var.columns;
                for(uint32_t r = 0; r < var.rows; r++)
                {
                  // read the column into the destination ShaderValue, which is tightly packed with
                  // rows
                  this->apiWrapper->ReadBufferValue(binding, offset + r * matrixStride, rowSize,
                                                    VarElemPointer(var, r * var.columns));
                }
              }
            }
          };

          if(isArray)
          {
            if(arraySize == ~0U)
            {
              RDCERR("Unsupported runtime array of UBOs");
              arraySize = 1;
            }

            var.members.reserve(arraySize);

            for(uint32_t a = 0; a < arraySize; a++)
            {
              binding.arrayElement = a;
              var.members.push_back(ShaderVariable());
              var.members.back().name = StringFormat::Fmt("[%u]", a);
              WalkVariable<ShaderVariable, true>(decorations[v.id], *innertype, 0U, false,
                                                 var.members.back(), rdcstr(), cbufferCallback);
            }
          }
          else
          {
            WalkVariable<ShaderVariable, true>(decorations[v.id], *innertype, 0U, false, var,
                                               rdcstr(), cbufferCallback);
          }

          sourceVar.type = VarType::ConstantBlock;
          sourceVar.rows = 1;
          sourceVar.columns = 1;
          sourceVar.variables.push_back(DebugVariableReference(DebugVariableType::Constant, var.name));

          global.constantBlocks.push_back(var);
          pointerIDs.push_back(GLOBAL_POINTER(v.id, constantBlocks));
        }

        ret->sourceVars.push_back(sourceVar);
      }
      else
      {
        RDCERR("Unhandled type of uniform: %u", innertype->type);
      }
    }
    else if(v.storage == StorageClass::UniformConstant)
    {
      if(!patchData.usedIds.contains(v.id))
        continue;

      // only images/samplers are allowed to be in UniformConstant in Vulkan SPIR-V. In GL SPIR-V
      // these can also be values, but we default to this and override below as needed
      ShaderVariable var;
      var.rows = 1;
      var.columns = 1;
      var.name = GetRawName(v.id);

      rdcstr sourceName = GetHumanName(v.id);

      const DataType &type = dataTypes[v.type];

      // global variables should all be pointers into opaque storage
      RDCASSERT(type.type == DataType::PointerType);

      const DataType *innertype = &dataTypes[type.InnerType()];

      // if we don't have a good human name, generate a better one using the interface information
      // we have
      if(sourceName == var.name)
      {
        rdcstr innerName;
        if(innertype->type == DataType::SamplerType)
          innerName = "sampler";
        else if(innertype->type == DataType::SampledImageType)
          innerName = "sampledImage";
        else if(innertype->type == DataType::ImageType)
          innerName = "image";
        else if(innertype->type == DataType::AccelerationStructureType)
          innerName = "accelerationStructure";
        sourceName = StringFormat::Fmt("_%s_set%u_bind%u", innerName.c_str(), decorations[v.id].set,
                                       decorations[v.id].binding);
      }

      DebugVariableType debugType = DebugVariableType::ReadOnlyResource;

      uint32_t set = 0, bind = 0, location = ~0U;
      if(decorations[v.id].flags & Decorations::HasDescriptorSet)
        set = decorations[v.id].set;
      if(decorations[v.id].flags & Decorations::HasBinding)
        bind = decorations[v.id].binding;
      if(decorations[v.id].flags & Decorations::HasLocation)
        location = decorations[v.id].location;

      // don't step into arrays when they're bare uniforms with locations
      if(innertype->type == DataType::ArrayType && location == ~0U)
      {
        enablePointerFlags(var, PointerFlags::GlobalArrayBinding);
        innertype = &dataTypes[innertype->InnerType()];
      }

      bool bareUniform = false;
      // GL Resource Arrays
      if((api->GetGraphicsAPI() == GraphicsAPI::OpenGL) &&
         (innertype->type == DataType::ArrayType && location != ~0U))
      {
        DataType *elementtype = &dataTypes[innertype->InnerType()];
        DataType::Type baseType = elementtype->type;
        if((baseType == DataType::SampledImageType) || (baseType == DataType::ImageType) ||
           (baseType == DataType::SamplerType))
        {
          var.type = VarType::Struct;
          debugType = DebugVariableType::ReadOnlyResource;

          VarType memberType = VarType::ReadOnlyResource;
          DescriptorCategory descCat = DescriptorCategory::ReadOnlyResource;
          uint32_t texType = DebugAPIWrapper::Float_Texture;

          if(baseType == DataType::SamplerType)
          {
            debugType = DebugVariableType::Sampler;
            memberType = VarType::Sampler;
            descCat = DescriptorCategory::Sampler;
          }
          else if(baseType == DataType::SampledImageType || baseType == DataType::ImageType)
          {
            // store the texture type here, since the image may be copied around and combined with
            // a sampler, so accessing the original type might be non-trivial at point of access
            Id imgid = elementtype->id;

            if(baseType == DataType::SampledImageType)
              imgid = sampledImageTypes[imgid].baseId;

            RDCASSERT(imageTypes[imgid].dim != Dim::Max);

            if(imageTypes[imgid].dim == Dim::Buffer)
              texType |= DebugAPIWrapper::Buffer_Texture;

            if(imageTypes[imgid].dim == Dim::SubpassData)
              texType |= DebugAPIWrapper::Subpass_Texture;

            if(imageTypes[imgid].retType.type == Op::TypeInt)
            {
              if(imageTypes[imgid].retType.signedness)
                texType |= DebugAPIWrapper::SInt_Texture;
              else
                texType |= DebugAPIWrapper::UInt_Texture;
            }

            if(imageTypes[imgid].sampled == 2 && imageTypes[imgid].dim != Dim::SubpassData)
            {
              debugType = DebugVariableType::ReadWriteResource;
              memberType = VarType::ReadWriteResource;
              descCat = DescriptorCategory::ReadWriteResource;
            }
          }
          int32_t idx = -1;
          if(memberType == VarType::ReadOnlyResource)
            idx = patchData.roInterface.indexOf(v.id);
          else if(memberType == VarType::Sampler)
            idx = patchData.samplerInterface.indexOf(v.id);
          else if(memberType == VarType::ReadWriteResource)
            idx = patchData.rwInterface.indexOf(v.id);

          uint32_t len = uintComp(GetActiveLane().ids[innertype->length], 0);
          for(uint32_t i = 0; i < len; ++i)
          {
            ShaderVariable member;

            member.rows = 1;
            member.columns = 1;
            member.name = StringFormat::Fmt("[%u]", i);
            member.type = memberType;

            if((memberType == VarType::ReadOnlyResource) ||
               (memberType == VarType::ReadWriteResource))
              setTextureType(member, (DebugAPIWrapper::TextureType)texType);

            // on GL we may have textures which are dead-code eliminated but remain part of the simulated
            // code. Because we base our interfaces off the GLSL reflected data it may not be present.

            // Bind to index "idx+i" because GL resource arrays are expanded in element order
            if(idx >= 0)
              member.SetBindIndex(ShaderBindIndex(descCat, idx + i, 0));
            else
              member.SetBindIndex(ShaderBindIndex());

            var.members.push_back(member);

            // Source mapping per array element because GL resource arrays are expanded
            SourceVariableMapping sourceVar;
            sourceVar.name = StringFormat::Fmt("%s[%u]", sourceName.c_str(), i);
            sourceVar.type = var.members[i].type;
            sourceVar.rows = 1;
            sourceVar.columns = 1;
            sourceVar.offset = 0;
            sourceVar.variables.push_back(DebugVariableReference(
                debugType, StringFormat::Fmt("%s[%u]", var.name.c_str(), i)));
            ret->sourceVars.push_back(sourceVar);
          }

          if(debugType == DebugVariableType::ReadOnlyResource)
          {
            global.readOnlyResources.push_back(var);
            pointerIDs.push_back(GLOBAL_POINTER(v.id, readOnlyResources));
          }
          else if(debugType == DebugVariableType::Sampler)
          {
            global.samplers.push_back(var);
            pointerIDs.push_back(GLOBAL_POINTER(v.id, samplers));
          }
          else if(debugType == DebugVariableType::ReadWriteResource)
          {
            global.readWriteResources.push_back(var);
            pointerIDs.push_back(GLOBAL_POINTER(v.id, readWriteResources));
          }
          continue;
        }
      }

      if(innertype->type == DataType::SamplerType)
      {
        var.type = VarType::Sampler;
        debugType = DebugVariableType::Sampler;

        int32_t idx = patchData.samplerInterface.indexOf(v.id);

        // on GL we may have samplers which are dead-code eliminated but remain part of the simulated
        // code. Because we base our interfaces off the GLSL reflected data it may not be present
        if(idx >= 0)
          var.SetBindIndex(ShaderBindIndex(DescriptorCategory::Sampler, idx, 0U));
        else
          var.SetBindIndex(ShaderBindIndex());

        if(api->GetGraphicsAPI() == GraphicsAPI::Vulkan)
          RDCASSERT(idx >= 0);

        global.samplers.push_back(var);
        pointerIDs.push_back(GLOBAL_POINTER(v.id, samplers));
      }
      else if(innertype->type == DataType::SampledImageType || innertype->type == DataType::ImageType)
      {
        var.type = VarType::ReadOnlyResource;
        debugType = DebugVariableType::ReadOnlyResource;

        // store the texture type here, since the image may be copied around and combined with a
        // sampler, so accessing the original type might be non-trivial at point of access
        uint32_t texType = DebugAPIWrapper::Float_Texture;

        Id imgid = innertype->id;

        if(innertype->type == DataType::SampledImageType)
          imgid = sampledImageTypes[imgid].baseId;

        RDCASSERT(imageTypes[imgid].dim != Dim::Max);

        if(imageTypes[imgid].dim == Dim::Buffer)
          texType |= DebugAPIWrapper::Buffer_Texture;

        if(imageTypes[imgid].dim == Dim::SubpassData)
          texType |= DebugAPIWrapper::Subpass_Texture;

        if(imageTypes[imgid].retType.type == Op::TypeInt)
        {
          if(imageTypes[imgid].retType.signedness)
            texType |= DebugAPIWrapper::SInt_Texture;
          else
            texType |= DebugAPIWrapper::UInt_Texture;
        }

        setTextureType(var, (DebugAPIWrapper::TextureType)texType);

        if(imageTypes[imgid].sampled == 2 && imageTypes[imgid].dim != Dim::SubpassData)
        {
          var.type = VarType::ReadWriteResource;
          debugType = DebugVariableType::ReadWriteResource;

          int32_t idx = patchData.rwInterface.indexOf(v.id);

          // on GL we may have textures which are dead-code eliminated but remain part of the simulated
          // code. Because we base our interfaces off the GLSL reflected data it may not be present
          if(idx >= 0)
            var.SetBindIndex(ShaderBindIndex(DescriptorCategory::ReadWriteResource, idx, 0U));
          else
            var.SetBindIndex(ShaderBindIndex());

          if(api->GetGraphicsAPI() == GraphicsAPI::Vulkan)
            RDCASSERT(idx >= 0);

          global.readWriteResources.push_back(var);
          pointerIDs.push_back(GLOBAL_POINTER(v.id, readWriteResources));
        }
        else
        {
          int32_t idx = patchData.roInterface.indexOf(v.id);

          // on GL we may have textures which are dead-code eliminated but remain part of the simulated
          // code. Because we base our interfaces off the GLSL reflected data it may not be present
          if(idx >= 0)
            var.SetBindIndex(ShaderBindIndex(DescriptorCategory::ReadOnlyResource, idx, 0U));
          else
            var.SetBindIndex(ShaderBindIndex());

          if(api->GetGraphicsAPI() == GraphicsAPI::Vulkan)
            RDCASSERT(idx >= 0);

          global.readOnlyResources.push_back(var);
          pointerIDs.push_back(GLOBAL_POINTER(v.id, readOnlyResources));
        }
      }
      else if(innertype->type == DataType::AccelerationStructureType)
      {
        var.type = VarType::ReadOnlyResource;
        debugType = DebugVariableType::ReadOnlyResource;

        global.readOnlyResources.push_back(var);
        pointerIDs.push_back(GLOBAL_POINTER(v.id, readOnlyResources));
      }
      else if(innertype->type == DataType::StructType || innertype->type == DataType::ArrayType ||
              innertype->type == DataType::MatrixType || innertype->type == DataType::VectorType ||
              innertype->type == DataType::ScalarType)
      {
        // plain variable
        bareUniform = true;

        // if we haven't already added a virtual uniforms cbuffer, do so now
        if(uniformsCBuffer == ~0U)
        {
          ShaderVariable uniformsVar;
          uniformsVar.rows = 1;
          uniformsVar.columns = 1;
          uniformsVar.type = VarType::ConstantBlock;

          SourceVariableMapping sourceVar;
          sourceVar.name = uniformsVar.name = "uniforms";
          sourceVar.type = VarType::ConstantBlock;
          sourceVar.rows = 1;
          sourceVar.columns = 1;
          sourceVar.offset = 0;
          sourceVar.variables.push_back(
              DebugVariableReference(DebugVariableType::Constant, uniformsVar.name));

          uniformsCBuffer = global.constantBlocks.size();

          global.constantBlocks.push_back(uniformsVar);
          pointerIDs.push_back(GLOBAL_POINTER(v.id, constantBlocks));

          ret->sourceVars.push_back(sourceVar);
        }

        rdcarray<ShaderVariable> &uniforms = global.constantBlocks[uniformsCBuffer].members;

        // record that this variable id needs to be pointed to the n'th member of the virtual
        // cbuffer, which we're about to add
        bareUniformPointers.push_back({v.id, uniforms.size()});

        var = ShaderVariable();
        var.name = GetHumanName(v.id);

        auto uniformCallback = [this](ShaderVariable &var, const Decorations &curDecorations,
                                      const DataType &type, uint64_t location, const rdcstr &) {
          if(var.members.empty())
            this->apiWrapper->ReadLocationValue((uint32_t)location, var);
        };

        WalkVariable<ShaderVariable, true>(decorations[v.id], *innertype, ~0U, false, var, rdcstr(),
                                           uniformCallback);

        uniforms.push_back(var);
      }
      else
      {
        RDCERR("Unhandled type of uniform: %u", innertype->type);
      }

      if(!bareUniform)
      {
        SourceVariableMapping sourceVar;
        sourceVar.name = sourceName;
        sourceVar.type = var.type;
        sourceVar.rows = 1;
        sourceVar.columns = 1;
        sourceVar.offset = 0;
        sourceVar.variables.push_back(DebugVariableReference(debugType, var.name));

        ret->sourceVars.push_back(sourceVar);
      }
    }
    else if(v.storage == StorageClass::Private || v.storage == StorageClass::Workgroup)
    {
      // private variables are allocated as globals. Similar to outputs
      ShaderVariable var;
      var.name = GetRawName(v.id);

      rdcstr sourceName = GetHumanName(v.id);

      const DataType &type = dataTypes[v.type];

      // global variables should all be pointers into opaque storage
      RDCASSERT(type.type == DataType::PointerType);

      auto uninitialisedCallback = [](ShaderVariable &var, const Decorations &, const DataType &,
                                      uint64_t, const rdcstr &) {
        if(!var.members.empty())
          return;

        memset(&var.value, 0xcc, sizeof(var.value));
      };

      WalkVariable<ShaderVariable, true>(decorations[v.id], dataTypes[type.InnerType()], ~0U, false,
                                         var, rdcstr(), uninitialisedCallback);

      if(v.initializer != Id())
        AssignValue(var, active.ids[v.initializer]);

      if(v.storage == StorageClass::Private)
      {
        active.privates.push_back(var);
        pointerIDs.push_back(THREAD_POINTER(v.id, privates));
      }
      else if(v.storage == StorageClass::Workgroup)
      {
        active.gsmIndexes.push_back({global.workgroups.count(), active.privates.count()});
        active.privates.push_back(var);
        global.workgroups.push_back(var);
        pointerIDs.push_back(GSM_POINTER(v.id, workgroups, privates));
      }

      liveGlobals.push_back(v.id);

      if(sourceName != var.name && (!m_DebugInfo.valid || m_DebugInfo.globals.contains(v.id)))
      {
        SourceVariableMapping sourceVar;
        sourceVar.name = sourceName;
        sourceVar.type = var.type;
        sourceVar.rows = RDCMAX(1U, (uint32_t)var.rows);
        sourceVar.columns = RDCMAX(1U, (uint32_t)var.columns);
        sourceVar.offset = 0;
        for(uint32_t x = 0; x < sourceVar.rows * sourceVar.columns; x++)
          sourceVar.variables.push_back(
              DebugVariableReference(DebugVariableType::Variable, var.name, x));

        ret->sourceVars.push_back(sourceVar);
      }
    }
    else
    {
      RDCERR("Unhandled type of global variable: %s", ToStr(v.storage).c_str());
    }
  }

  std::sort(liveGlobals.begin(), liveGlobals.end());

  rdcarray<ThreadIndex> threadIds;
  for(uint32_t i = 0; i < threadsInWorkgroup; i++)
  {
    bool isActiveLane = (i == activeLaneIndex);
    ThreadState &lane = workgroup[i];
    lane.workgroupIndex = i;
    lane.activeMask.resize(threadsInWorkgroup);
    if(!isActiveLane)
    {
      lane.nextInstruction = active.nextInstruction;
      lane.outputs = active.outputs;
      lane.privates = active.privates;
      lane.ids = active.ids;
    }

    if(stage == ShaderStage::Pixel)
    {
      lane.helperInvocation = apiWrapper->GetThreadProperty(i, ThreadProperty::Helper) != 0;
      lane.quadLaneIndex = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadLane);
      lane.quadId = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadId);
    }
    if(stage == ShaderStage::Compute)
    {
      lane.quadLaneIndex = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadLane);
      lane.quadId = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadId);
    }

    lane.subgroupId = apiWrapper->GetThreadProperty(i, ThreadProperty::SubgroupId);
    lane.dead = apiWrapper->GetThreadProperty(i, ThreadProperty::Active) == 0;

    if(patchData.threadScope & ThreadScope::Subgroup)
      lane.elected = apiWrapper->GetThreadProperty(i, ThreadProperty::Elected) != 0;

    // now that the globals are allocated and their storage won't move, we can take pointers to them
    for(const PointerId &p : pointerIDs)
      p.Set(*this, global, lane, isActiveLane);

    for(const rdcpair<rdcspv::Id, size_t> &u : bareUniformPointers)
    {
      lane.ids[u.first] =
          MakePointerVariable(u.first, &global.constantBlocks[uniformsCBuffer].members[u.second]);
    }

    if(isActiveLane)
    {
      for(const PointerId &p : pointerIDs)
      {
        // GSM pointers have a global and local index
        // Create a GSM global pointer, used for writing back
        if((p.globalIndex != UINT_MAX) && (p.threadIndex != UINT_MAX))
        {
          RDCASSERTEQUAL(lane.gsmPointers.count(p.id), 0);
          lane.gsmPointers[p.id] = MakePointerVariable(p.id, &global.workgroups[p.globalIndex]);
        }
      }
    }

    // Only add active lanes to control flow
    if(!lane.dead)
      threadIds.push_back(i);
  }

  controlFlow.Construct(threadIds);

  // find quad neighbours
  {
    rdcarray<uint32_t> processedQuads;
    for(uint32_t i = 0; i < threadsInWorkgroup; i++)
    {
      uint32_t desiredQuad = workgroup[i].quadId;

      // ignore threads not in any quad
      if(desiredQuad == 0)
        continue;

      // quads are almost certainly sorted together, so shortcut by checking the last one
      if((!processedQuads.empty() && processedQuads.back() == desiredQuad) ||
         processedQuads.contains(desiredQuad))
        continue;

      processedQuads.push_back(desiredQuad);

      // find the threads
      uint32_t threads[4] = {
          i,
          ~0U,
          ~0U,
          ~0U,
      };
      for(uint32_t j = i + 1, t = 1; j < threadsInWorkgroup && t < 4; j++)
      {
        if(workgroup[j].quadId == desiredQuad)
          threads[t++] = j;
      }

      // now swizzle the threads to know each other
      for(uint32_t src = 0; src < 4; src++)
      {
        const uint32_t thread = threads[src];

        if(thread >= workgroup.size())
        {
          RDCERR("Unexpected incomplete quad missing a thread");
          continue;
        }

        const uint32_t lane = workgroup[thread].quadLaneIndex;

        if(lane >= 4)
          continue;

        for(uint32_t dst = 0; dst < 4; dst++)
        {
          if(threads[dst] == ~0U)
            continue;

          workgroup[threads[dst]].quadNeighbours[lane] = threads[src];
        }
      }
    }
  }

  // this contains all the accumulated line number information. Add in our disassembly mapping
  ret->instInfo = m_InstInfo;
  for(size_t i = 0; i < m_InstInfo.size(); i++)
  {
    auto it = instructionLines.find(instructionOffsets[m_InstInfo[i].instruction]);
    if(it != instructionLines.end())
      ret->instInfo[i].lineInfo.disassemblyLine = it->second;
    else
      ret->instInfo[i].lineInfo.disassemblyLine = 0;
  }

  if(m_DebugInfo.valid)
    FillDebugSourceVars(ret->instInfo);
  else
    FillDefaultSourceVars(ret->instInfo);

  ret->constantBlocks = global.constantBlocks;
  ret->readOnlyResources = global.readOnlyResources;
  ret->readWriteResources = global.readWriteResources;
  ret->samplers = global.samplers;
  ret->inputs = active.inputs;

  mtSimulation = apiWrapper->SimulateThreaded();
  if(threadsInWorkgroup < 4)
    mtSimulation = false;

  AtomicStore(&atomic_simulationFinished, 0);
  if(mtSimulation)
  {
    if(!Shader_Debug_UseJobSystemJobs())
    {
      uint32_t countJobs = RDCMIN(threadsInWorkgroup, Threading::JobSystem::GetCountWorkers() / 2U);
      for(uint32_t i = 0; i < countJobs; ++i)
        Threading::JobSystem::AddJob([this]() { SimulationJobHelper(); });
    }
  }
  return ret;
}

void Debugger::FillCallstack(ThreadState &thread, ShaderDebugState &state) const
{
  rdcarray<Id> funcs;
  thread.FillCallstack(funcs);

  for(Id f : funcs)
  {
    if(m_DebugInfo.valid)
    {
      auto it = m_DebugInfo.funcToDebugFunc.find(f);
      if(it != m_DebugInfo.funcToDebugFunc.end())
      {
        state.callstack.push_back(m_DebugInfo.scopes[it->second].name);
        continue;
      }
    }

    state.callstack.push_back(GetHumanName(f));
  }
}

void Debugger::FillDebugSourceVars(rdcarray<InstructionSourceInfo> &instInfo) const
{
  for(InstructionSourceInfo &i : instInfo)
  {
    size_t offs = instructionOffsets[i.instruction];

    const ScopeData *scope = GetScope(offs);

    if(!scope)
      continue;

    // track which mappings we've processed, so if the same variable has mappings in multiple scopes
    // we only pick the innermost.
    rdcarray<LocalMapping> processed;
    rdcarray<Id> sourceVars;

    // capture the scopes upwards (from child to parent)
    rdcarray<const ScopeData *> scopes;
    while(scope)
    {
      scopes.push_back(scope);
      // if we reach a function scope, don't go up any further.
      if(scope->type == DebugScope::Function)
        break;

      scope = scope->parent;
    }

    // Iterate over the scopes downwards (parent->child)
    for(size_t s = 0; s < scopes.size(); ++s)
    {
      scope = scopes[scopes.size() - 1 - s];
      for(size_t m = 0; m < scope->localMappings.size(); m++)
      {
        const LocalMapping &mapping = scope->localMappings[m];

        // if this mapping is past the current instruction, stop here.
        if(mapping.instIndex > i.instruction)
          break;

        // see if this mapping is superceded by a later mapping in this scope for this instruction.
        // This is a bit inefficient but simple. The alternative would be to do record
        // start and end points for each mapping and update the end points, but this is simple and
        // should be limited since it's only per-scope
        bool supercede = false;
        for(size_t n = m + 1; n < scope->localMappings.size(); n++)
        {
          const LocalMapping &laterMapping = scope->localMappings[n];

          // if this mapping is past the current instruction, stop here.
          if(laterMapping.instIndex > i.instruction)
            break;

          // if this mapping will supercede and starts later
          if(laterMapping.isSourceSupersetOf(mapping) && laterMapping.instIndex > mapping.instIndex)
          {
            supercede = true;
            break;
          }
        }

        // don't add the current mapping if it's going to be superceded by something later
        if(supercede)
          continue;

        processed.push_back(mapping);
        Id sourceVar = mapping.sourceVar;
        if(!sourceVars.contains(mapping.sourceVar))
          sourceVars.push_back(mapping.sourceVar);
      }
    }

    // Converting debug variable mappings to SourceVariableMapping is a two phase algorithm.

    // Phase One
    // For each source variable, repeatedly apply the debug variable mappings.
    // This debug variable usage is tracked in a tree-like structure built using DebugVarNode
    // elements.
    // As each mapping is applied, the new mapping can fully or partially override the
    // existing mapping. When an existing mapping is:
    //  - fully overrideen: any sub-elements of that mapping are cleared
    //    i.e. assigning a vector, array, structure
    //  - partially overriden: the existing mapping is expanded into its sub-elements which are
    //    mapped to the current mapping and then the new mapping is set to its corresponding
    //    elements i.e. y-component in a vector, member in a structure, a single array element
    // The DebugVarNode member "emitSourceVar" determines if the DebugVar mapping should be
    // converted to a source variable mapping.

    // Phase Two
    // The DebugVarNode tree is walked to find the nodes which have "emitSourceVar" set to true and
    // then those nodes are converted to SourceVariableMapping

    struct DebugVarNode
    {
      rdcarray<DebugVarNode> children;
      Id debugVar;
      rdcstr name;
      rdcstr debugVarSuffix;
      VarType type = VarType::Unknown;
      uint32_t rows = 0;
      uint32_t columns = 0;
      uint32_t debugVarComponent = 0;
      uint32_t offset = 0;
      bool emitSourceVar = false;
    };

    ::std::map<Id, DebugVarNode> roots;

    // Phase One: generate the DebugVarNode tree by repeatedly apply debug variables updating
    // existing mappings with later mappings
    for(size_t sv = 0; sv < sourceVars.size(); ++sv)
    {
      Id sourceVarId = sourceVars[sv];
      const LocalData &l = m_DebugInfo.locals[sourceVarId];

      // Convert processed mappings into a usage map
      for(size_t m = 0; m < processed.size(); ++m)
      {
        const LocalMapping &mapping = processed[m];
        if(mapping.sourceVar != sourceVarId)
          continue;

        const TypeData *typeWalk = l.type;
        DebugVarNode *usage = &roots[sourceVarId];
        if(usage->name.isEmpty())
        {
          usage->name = l.name;
          usage->rows = 1U;
          usage->columns = 1U;
        }

        // if it doesn't have indexes this is simple, set up a 1:1 map
        if(mapping.indexes.isEmpty())
        {
          uint32_t rows = 1;
          uint32_t columns = 1;
          // skip past any pointer types to get the 'real' type that we'll see
          while(typeWalk && typeWalk->baseType != Id() && typeWalk->type == VarType::GPUPointer)
            typeWalk = &m_DebugInfo.types[typeWalk->baseType];

          const uint32_t arrayDimension = typeWalk->arrayDimensions.size();
          if(arrayDimension > 0)
          {
            // walk down until we get to a scalar type, if we get there. This means arrays of
            // basic types will get the right type
            while(typeWalk && typeWalk->baseType != Id() && typeWalk->type == VarType::Unknown)
              typeWalk = &m_DebugInfo.types[typeWalk->baseType];

            usage->type = typeWalk->type;
          }
          else if(!typeWalk->structMembers.empty())
          {
            usage->type = typeWalk->type;
          }
          if(typeWalk->matSize != 0)
          {
            const TypeData &vec = m_DebugInfo.types[typeWalk->baseType];
            const TypeData &scalar = m_DebugInfo.types[vec.baseType];

            usage->type = scalar.type;

            if(typeWalk->colMajorMat)
            {
              rows = RDCMAX(1U, vec.vecSize);
              columns = RDCMAX(1U, typeWalk->matSize);
            }
            else
            {
              columns = RDCMAX(1U, vec.vecSize);
              rows = RDCMAX(1U, typeWalk->matSize);
            }
          }
          else if(typeWalk->vecSize != 0)
          {
            const TypeData &scalar = m_DebugInfo.types[typeWalk->baseType];

            usage->type = scalar.type;
            columns = RDCMAX(1U, typeWalk->vecSize);
          }
          else
          {
            usage->type = typeWalk->type;
          }

          usage->debugVar = mapping.debugVar;
          // Remove any child mappings : this mapping covers everything
          usage->children.clear();
          usage->emitSourceVar = true;
          usage->rows = rows;
          usage->columns = columns;
        }
        else
        {
          rdcarray<uint32_t> indexes = mapping.indexes;

          // walk any aggregate types
          while(!indexes.empty())
          {
            uint32_t idx = ~0U;
            const TypeData *childType = NULL;
            const uint32_t arrayDimension = typeWalk->arrayDimensions.size();
            if(arrayDimension > 0)
            {
              const rdcarray<uint32_t> &dims = typeWalk->arrayDimensions;
              uint32_t numIdxs = (uint32_t)indexes.size();
              childType = &m_DebugInfo.types[typeWalk->baseType];
              uint32_t childRows = 1U;
              uint32_t childColumns = 1U;
              VarType elementType = childType->type;
              uint32_t elementSize = 1;
              if(childType->matSize != 0)
              {
                const TypeData &vec = m_DebugInfo.types[childType->baseType];
                const TypeData &scalar = m_DebugInfo.types[vec.baseType];

                elementType = scalar.type;
                if(childType->colMajorMat)
                {
                  childRows = RDCMAX(1U, vec.vecSize);
                  childColumns = RDCMAX(1U, childType->matSize);
                }
                else
                {
                  childColumns = RDCMAX(1U, vec.vecSize);
                  childRows = RDCMAX(1U, childType->matSize);
                }
              }
              else if(childType->vecSize != 0)
              {
                const TypeData &scalar = m_DebugInfo.types[childType->baseType];
                uint32_t vecColumns = RDCMAX(1U, childType->vecSize);

                elementType = scalar.type;

                childRows = 1U;
                childColumns = vecColumns;
              }
              else if(!childType->structMembers.empty())
              {
                elementSize += childType->memberOffsets[childType->memberOffsets.count() - 1];
              }
              elementSize *= childRows * childColumns;
              const uint32_t countDims = RDCMIN(arrayDimension, numIdxs);
              // handle N dimensional arrays
              for(uint32_t d = 0; d < countDims; ++d)
              {
                idx = indexes[0];
                indexes.erase(0);
                uint32_t rows = dims[d];
                usage->rows = rows;
                usage->columns = 1U;
                // Expand the node if required
                if(usage->children.isEmpty())
                {
                  usage->children.resize(rows);
                  for(uint32_t x = 0; x < rows; x++)
                  {
                    usage->children[x].debugVar = usage->debugVar;
                    rdcstr suffix = StringFormat::Fmt("[%u]", x);
                    usage->children[x].debugVarSuffix = usage->debugVarSuffix + suffix;
                    usage->children[x].name = usage->name + suffix;
                    usage->children[x].type = elementType;
                    usage->children[x].rows = childRows;
                    usage->children[x].columns = childColumns;
                    usage->children[x].offset = usage->offset + x * elementSize;
                  }
                }
                RDCASSERTEQUAL(usage->children.size(), rows);
                // if the whole node was displayed : display the sub-elements
                if(usage->emitSourceVar)
                {
                  for(uint32_t x = 0; x < rows; x++)
                    usage->children[x].emitSourceVar = true;
                  usage->emitSourceVar = false;
                }
                usage = &usage->children[idx];
                usage->type = childType->type;
                typeWalk = childType;
              }
            }
            else if(!typeWalk->structMembers.empty())
            {
              idx = indexes[0];
              indexes.erase(0);
              childType = &m_DebugInfo.types[typeWalk->structMembers[idx].second];
              uint32_t rows = typeWalk->structMembers.size();
              usage->rows = rows;
              usage->columns = 1U;
              // Expand the node if required
              if(usage->children.isEmpty())
              {
                usage->children.resize(rows);
                for(uint32_t x = 0; x < rows; x++)
                {
                  rdcstr suffix = StringFormat::Fmt(".%s", typeWalk->structMembers[x].first.c_str());
                  usage->children[x].debugVar = usage->debugVar;
                  usage->children[x].debugVarSuffix = usage->debugVarSuffix + suffix;
                  usage->children[x].name = usage->name + suffix;
                  usage->children[x].offset = usage->offset + typeWalk->memberOffsets[x];
                  uint32_t memberRows = 1U;
                  uint32_t memberColumns = 1U;
                  const TypeData *memberType = &m_DebugInfo.types[typeWalk->structMembers[x].second];
                  VarType elementType = memberType->type;
                  if(memberType->matSize != 0)
                  {
                    const TypeData &vec = m_DebugInfo.types[memberType->baseType];
                    const TypeData &scalar = m_DebugInfo.types[vec.baseType];

                    elementType = scalar.type;
                    if(memberType->colMajorMat)
                    {
                      memberRows = RDCMAX(1U, vec.vecSize);
                      memberColumns = RDCMAX(1U, memberType->matSize);
                    }
                    else
                    {
                      memberColumns = RDCMAX(1U, vec.vecSize);
                      memberRows = RDCMAX(1U, memberType->matSize);
                    }
                  }
                  else if(memberType->vecSize != 0)
                  {
                    const TypeData &scalar = m_DebugInfo.types[memberType->baseType];
                    uint32_t vecColumns = RDCMAX(1U, memberType->vecSize);

                    elementType = scalar.type;

                    memberRows = 1U;
                    memberColumns = vecColumns;
                  }
                  usage->children[x].type = elementType;
                  usage->children[x].rows = memberRows;
                  usage->children[x].columns = memberColumns;
                }
              }
              RDCASSERTEQUAL(usage->children.size(), rows);
              // if the whole node was displayed : display the sub-elements
              if(usage->emitSourceVar)
              {
                for(uint32_t x = 0; x < rows; x++)
                  usage->children[x].emitSourceVar = true;
                usage->emitSourceVar = false;
              }

              usage = &usage->children[idx];
              usage->type = childType->type;
              typeWalk = childType;
            }
            else
            {
              break;
            }
          }

          const char swizzle[] = "xyzw";
          uint32_t rows = 1U;
          uint32_t columns = 1U;
          size_t countRemainingIndexes = indexes.size();
          if(typeWalk->matSize != 0)
          {
            const TypeData &vec = m_DebugInfo.types[typeWalk->baseType];
            const TypeData &scalar = m_DebugInfo.types[vec.baseType];

            usage->type = scalar.type;

            if(typeWalk->colMajorMat)
            {
              rows = RDCMAX(1U, vec.vecSize);
              columns = RDCMAX(1U, typeWalk->matSize);
            }
            else
            {
              columns = RDCMAX(1U, vec.vecSize);
              rows = RDCMAX(1U, typeWalk->matSize);
            }
            usage->rows = rows;
            usage->columns = columns;

            if((countRemainingIndexes == 2) || (countRemainingIndexes == 1))
            {
              if(usage->children.isEmpty())
              {
                // Matrices are stored as [row][col]
                usage->children.resize(rows);
                for(uint32_t r = 0; r < rows; ++r)
                {
                  usage->children[r].emitSourceVar = false;
                  usage->children[r].name = usage->name + StringFormat::Fmt(".row%u", r);
                  usage->children[r].type = scalar.type;
                  usage->children[r].debugVar = usage->debugVar;
                  usage->children[r].debugVarComponent = 0;
                  usage->children[r].rows = 1U;
                  usage->children[r].columns = columns;
                  usage->children[r].offset = usage->offset + r * rows;
                  usage->children[r].children.resize(columns);
                  for(uint32_t c = 0; c < columns; ++c)
                  {
                    usage->children[r].children[c].emitSourceVar = false;
                    usage->children[r].children[c].name =
                        usage->name + StringFormat::Fmt(".row%u.%c", r, swizzle[RDCMIN(c, 3U)]);
                    usage->children[r].children[c].type = scalar.type;
                    usage->children[r].children[c].debugVar = usage->debugVar;
                    usage->children[r].children[c].debugVarComponent = r;
                    usage->children[r].children[c].rows = 1U;
                    usage->children[r].children[c].columns = 1U;
                    usage->children[r].children[c].offset = usage->children[r].offset + c;
                  }
                }
              }
              RDCASSERTEQUAL(usage->children.size(), rows);

              // two remaining indices selects a scalar within the matrix
              if(countRemainingIndexes == 2)
              {
                uint32_t row, col;

                if(typeWalk->colMajorMat)
                {
                  col = indexes[0];
                  row = indexes[1];
                }
                else
                {
                  row = indexes[0];
                  col = indexes[1];
                }
                RDCASSERT(row < rows, row, rows);
                RDCASSERT(col < columns, col, columns);

                RDCASSERTEQUAL(usage->children[row].children.size(), columns);
                usage->children[row].children[col].emitSourceVar =
                    !usage->children[row].emitSourceVar;
                usage->children[row].children[col].debugVar = mapping.debugVar;
                usage->children[row].children[col].debugVarComponent = 0;

                // try to recombine matrix rows to a single source var display
                if(!usage->children[row].emitSourceVar)
                {
                  bool collapseVector = true;
                  for(uint32_t c = 0; c < columns; ++c)
                  {
                    collapseVector = usage->children[row].children[c].emitSourceVar;
                    if(!collapseVector)
                      break;
                  }
                  if(collapseVector)
                  {
                    usage->children[row].emitSourceVar = true;
                    for(uint32_t c = 0; c < columns; ++c)
                      usage->children[row].children[c].emitSourceVar = false;
                  }
                }
              }
              else
              {
                if(typeWalk->colMajorMat)
                {
                  uint32_t col = indexes[0];
                  RDCASSERT(col < columns, col, columns);
                  // one remaining index selects a column within the matrix.
                  // source vars are displayed as row-major, need <rows> mappings
                  for(uint32_t r = 0; r < rows; ++r)
                  {
                    RDCASSERTEQUAL(usage->children[r].children.size(), columns);
                    usage->children[r].children[col].emitSourceVar =
                        !usage->children[r].emitSourceVar;
                    usage->children[r].children[col].debugVar = mapping.debugVar;
                    usage->children[r].children[col].debugVarComponent = r;
                  }
                }
                else
                {
                  uint32_t row = indexes[0];
                  RDCASSERT(row < rows, row, rows);
                  RDCASSERTEQUAL(usage->children.size(), rows);
                  RDCASSERTEQUAL(usage->children[row].children.size(), columns);
                  // one remaining index selects a row within the matrix.
                  // source vars are displayed as row-major, need <rows> mappings
                  for(uint32_t c = 0; c < columns; ++c)
                  {
                    usage->children[row].children[c].emitSourceVar =
                        !usage->children[row].emitSourceVar;
                    usage->children[row].children[c].debugVar = mapping.debugVar;
                    usage->children[row].children[c].debugVarComponent = c;
                  }
                }
              }
              // try to recombine matrix rows to a single source var display
              for(uint32_t r = 0; r < rows; ++r)
              {
                if(!usage->children[r].emitSourceVar)
                {
                  bool collapseVector = true;
                  RDCASSERTEQUAL(usage->children[r].children.size(), columns);
                  for(uint32_t c = 0; c < columns; ++c)
                  {
                    collapseVector = usage->children[r].children[c].emitSourceVar;
                    if(!collapseVector)
                      break;
                  }
                  if(collapseVector)
                  {
                    usage->children[r].emitSourceVar = true;
                    for(uint32_t c = 0; c < columns; ++c)
                      usage->children[r].children[c].emitSourceVar = false;
                  }
                }
              }
              usage->emitSourceVar = false;
            }
            else
            {
              RDCASSERTEQUAL(countRemainingIndexes, 0);
              // Remove mappings : this mapping covers everything
              usage->debugVar = mapping.debugVar;
              usage->children.clear();
              usage->emitSourceVar = true;
              usage->debugVarSuffix.clear();
            }
          }
          else if(typeWalk->vecSize != 0)
          {
            const TypeData &scalar = m_DebugInfo.types[typeWalk->baseType];
            columns = RDCMAX(1U, typeWalk->vecSize);

            usage->type = scalar.type;

            usage->rows = 1U;
            usage->columns = columns;

            // remaining index selects a scalar within the vector
            if(countRemainingIndexes == 1)
            {
              if(usage->children.isEmpty())
              {
                usage->children.resize(columns);
                for(uint32_t x = 0; x < columns; ++x)
                {
                  usage->children[x].emitSourceVar = usage->emitSourceVar;
                  usage->children[x].name =
                      usage->name + StringFormat::Fmt(".%c", swizzle[RDCMIN(x, 3U)]);
                  usage->children[x].type = scalar.type;
                  usage->children[x].debugVar = usage->debugVar;
                  usage->children[x].debugVarComponent = x;
                  usage->children[x].rows = 1U;
                  usage->children[x].columns = 1U;
                  usage->children[x].offset = usage->offset + x;
                }
                usage->emitSourceVar = false;
              }
              uint32_t col = indexes[0];
              RDCASSERT(col < columns, col, columns);
              RDCASSERTEQUAL(usage->children.size(), columns);
              usage->children[col].debugVar = mapping.debugVar;
              usage->children[col].debugVarComponent = 0;
              usage->children[col].emitSourceVar = true;

              // try to recombine vector to a single source var display
              bool collapseVector = true;
              for(uint32_t x = 0; x < columns; ++x)
              {
                collapseVector = usage->children[x].emitSourceVar;
                if(!collapseVector)
                  break;
              }
              if(collapseVector)
              {
                usage->emitSourceVar = true;
                for(uint32_t x = 0; x < columns; ++x)
                  usage->children[x].emitSourceVar = false;
              }
            }
            else
            {
              RDCASSERTEQUAL(countRemainingIndexes, 0);
              // Remove mappings : this mapping covers everything
              usage->debugVar = mapping.debugVar;
              usage->children.clear();
              usage->emitSourceVar = true;
              usage->debugVarSuffix.clear();
            }
          }
          else
          {
            // walk down until we get to a scalar type, if we get there. This means arrays of
            // basic types will get the right type
            while(typeWalk && typeWalk->baseType != Id() && typeWalk->type == VarType::Unknown)
              typeWalk = &m_DebugInfo.types[typeWalk->baseType];

            usage->type = typeWalk->type;
            usage->debugVar = mapping.debugVar;
            usage->debugVarComponent = 0;
            usage->rows = 1U;
            usage->columns = 1U;
            usage->emitSourceVar = true;
            usage->children.clear();
            usage->debugVarSuffix.clear();
          }
        }
      }
    }

    // Phase Two: walk the DebugVarNode tree and convert "emitSourceVar = true" nodes to a SourceVariableMapping
    for(size_t sv = 0; sv < sourceVars.size(); ++sv)
    {
      Id sourceVarId = sourceVars[sv];
      DebugVarNode *usage = &roots[sourceVarId];
      rdcarray<const DebugVarNode *> nodesToProcess;
      rdcarray<const DebugVarNode *> sourceVarNodes;
      nodesToProcess.push_back(usage);
      while(!nodesToProcess.isEmpty())
      {
        const DebugVarNode *n = nodesToProcess.back();
        nodesToProcess.pop_back();
        if(n->emitSourceVar)
        {
          sourceVarNodes.push_back(n);
        }
        else
        {
          for(size_t x = 0; x < n->children.size(); ++x)
          {
            const DebugVarNode *child = &n->children[x];
            nodesToProcess.push_back(child);
          }
        }
      }
      for(size_t x = 0; x < sourceVarNodes.size(); ++x)
      {
        const DebugVarNode *n = sourceVarNodes[x];
        SourceVariableMapping sourceVar;
        sourceVar.name = n->name;
        sourceVar.type = n->type;
        sourceVar.rows = n->rows;
        sourceVar.columns = n->columns;
        sourceVar.signatureIndex = -1;
        sourceVar.offset = n->offset;
        sourceVar.variables.clear();
        // unknown is treated as a struct
        if(sourceVar.type == VarType::Unknown)
          sourceVar.type = VarType::Struct;

        if(n->children.empty())
        {
          ConstIter it = GetID(n->debugVar);

          if(it.opcode() == Op::Undef)
          {
            sourceVar.rows = sourceVar.columns = 1;
            sourceVar.undefinedValue = true;

            sourceVar.variables.push_back(DebugVariableReference(
                DebugVariableType::Variable, GetRawName(n->debugVar) + n->debugVarSuffix, 0));
          }
          else
          {
            RDCASSERTNOTEQUAL(n->rows * n->columns, 0);
            for(uint32_t c = 0; c < n->rows * n->columns; ++c)
            {
              sourceVar.variables.push_back(DebugVariableReference(
                  DebugVariableType::Variable, GetRawName(n->debugVar) + n->debugVarSuffix, c));
            }
          }
        }
        else
        {
          RDCASSERTEQUAL(n->rows * n->columns, (uint32_t)n->children.count());
          for(int32_t c = 0; c < n->children.count(); ++c)
            sourceVar.variables.push_back(DebugVariableReference(
                DebugVariableType::Variable,
                GetRawName(n->children[c].debugVar) + n->children[c].debugVarSuffix,
                n->children[c].debugVarComponent));
        }
        i.sourceVars.push_back(sourceVar);
      }
    }
  }
}

void Debugger::FillDefaultSourceVars(rdcarray<InstructionSourceInfo> &instInfo) const
{
  rdcarray<SourceVariableMapping> sourceVars;
  rdcarray<Id> debugVars;

  for(InstructionSourceInfo &i : instInfo)
  {
    // the source vars for this instruction are whatever we have currently, because when we're
    // looking up the source vars for instruction X we are effectively talking abotu the state just
    // before X executes, not just after.
    i.sourceVars = sourceVars;

    // now update the sourcevars for after this instruction executed

    size_t offs = instructionOffsets[i.instruction];

    ConstIter it(m_SPIRV, offs);

    OpDecoder opdata(it);

    Id id = opdata.result;

    // stores can bring their pointer into being, if it's the first write.
    if(opdata.op == Op::Store)
      id = OpStore(it).pointer;

    // if this is the offset where the id's live range begins, try to add the source name for it if
    // one exists.
    if(id != Id() && idLiveRange[id].first == offs)
    {
      rdcstr name;

      auto dyn = dynamicNames.find(id);
      if(dyn != dynamicNames.end())
        name = dyn->second;
      else
        name = strings[id];

      if(!name.empty())
      {
        SourceVariableMapping sourceVar;

        const DataType *type = &GetTypeForId(id);

        while(type->type == DataType::PointerType || type->type == DataType::ArrayType)
          type = &GetType(type->InnerType());

        sourceVar.name = name;
        sourceVar.offset = 0;
        if(type->type == DataType::MatrixType || type->type == DataType::VectorType ||
           type->type == DataType::ScalarType)
          sourceVar.type = type->scalar().Type();
        else if(type->type == DataType::StructType)
          sourceVar.type = VarType::Struct;
        else if(type->type == DataType::ImageType || type->type == DataType::SampledImageType ||
                type->type == DataType::SamplerType)
          sourceVar.type = VarType::ReadOnlyResource;
        sourceVar.rows = RDCMAX(1U, (uint32_t)type->matrix().count);
        sourceVar.columns = RDCMAX(1U, (uint32_t)type->vector().count);
        rdcstr rawName = GetRawName(id);
        for(uint32_t x = 0; x < sourceVar.rows * sourceVar.columns; x++)
          sourceVar.variables.push_back(
              DebugVariableReference(DebugVariableType::Variable, rawName, x));

        sourceVars.push_back(sourceVar);
        debugVars.push_back(id);
      }
    }

    // see which vars have expired
    for(size_t d = 0; d < debugVars.size();)
    {
      if(offs > idLiveRange[debugVars[d]].second)
      {
        sourceVars.erase(d);
        debugVars.erase(d);
        continue;
      }

      d++;
    }

    // all variables/IDs are function-local
    if(opdata.op == Op::FunctionEnd)
    {
      sourceVars.clear();
      debugVars.clear();
    }
  }
}

rdcarray<ShaderDebugState> Debugger::ContinueDebug()
{
  ThreadState &active = GetActiveLane();

  rdcarray<ShaderDebugState> ret;
  shaderChangesReturn = NULL;

  // initialise the first ShaderDebugState if we haven't stepped yet
  if(steps == 0)
  {
    ShaderDebugState initial;
    uint32_t startPoint = INVALID_EXECUTION_POINT;

    // we should be sitting at the entry point function prologue, step forward into the first block
    // and past any function-local variable declarations
    for(size_t lane = 0; lane < workgroup.size(); lane++)
    {
      ThreadState &thread = workgroup[lane];

      if(lane == activeLaneIndex)
      {
        thread.EnterEntryPoint(true);
        FillCallstack(thread, initial);
        initial.nextInstruction = thread.nextInstruction;
        const ShaderDebugState &pendingDebugState = thread.GetPendingDebugState();
        initial.flags = pendingDebugState.flags;
        initial.changes.append(pendingDebugState.changes);
        startPoint = initial.nextInstruction;
      }
      else
      {
        thread.EnterEntryPoint(false);
      }
    }

    // globals won't be filled out by entering the entry point, ensure their change is registered.
    ShaderVariable val;
    DeviceOpResult opResult;
    for(const Id &v : liveGlobals)
    {
      opResult = GetPointerValue(active.ids[v], val);
      RDCASSERTEQUAL(opResult, DeviceOpResult::Succeeded);
      initial.changes.push_back({ShaderVariable(), val});
    }

    if(m_DebugInfo.valid)
    {
      // debug info can refer to constants for source variable values. Add an initial change for any
      // that are so referenced
      for(const Id &v : m_DebugInfo.constants)
      {
        opResult = GetPointerValue(active.ids[v], val);
        RDCASSERTEQUAL(opResult, DeviceOpResult::Succeeded);
        initial.changes.push_back({ShaderVariable(), val});
      }
    }

    ret.push_back(std::move(initial));

    // Set the initial execution point for the threads in the root tangle
    ThreadExecutionStates threadExecutionStates;
    TangleGroup &tangles = controlFlow.GetTangles();
    RDCASSERTEQUAL(tangles.size(), 1);
    RDCASSERTNOTEQUAL(startPoint, INVALID_EXECUTION_POINT);
    for(Tangle &tangle : tangles)
    {
      RDCASSERT(tangle.IsAliveActive());
      for(uint32_t threadIdx = 0; threadIdx < workgroup.size(); ++threadIdx)
      {
        if(!workgroup[threadIdx].Finished())
          threadExecutionStates[threadIdx].push_back(startPoint);
      }
    }
    controlFlow.UpdateState(threadExecutionStates);
    steps++;
  }

  // if we've finished, return an empty set to signify that
  if(active.Finished())
  {
    AtomicStore(&atomic_simulationFinished, 1);
    Threading::JobSystem::SyncAllJobs();
    return ret;
  }

  bool allStepsCompleted = true;
  shaderChangesReturn = &ret;

  // continue stepping until we have 1000000 target steps completed in a chunk. This may involve
  // doing more steps if our target thread is inactive
  for(int stepEnd = steps + 1000000; steps < stepEnd;)
  {
    global.clock++;
    allStepsCompleted = true;
    if(active.Finished() && !active.IsSimulationStepActive())
      break;

    // Execute the threads in each active tangle
    ThreadExecutionStates threadExecutionStates;
    TangleGroup &tangles = controlFlow.GetTangles();

    bool anyActiveThreads = false;
    for(const Tangle &tangle : tangles)
    {
      if(!tangle.IsAliveActive())
        continue;

      rdcarray<bool> activeMask;
      // one bool per workgroup thread
      activeMask.resize(workgroup.size());

      // calculate the current active thread mask from the threads in the tangle
      for(size_t i = 0; i < workgroup.size(); i++)
        activeMask[i] = false;

      const rdcarray<ThreadReference> &threadRefs = tangle.GetThreadRefs();
      for(const ThreadReference &ref : threadRefs)
      {
        uint32_t lane = ref.id;
        RDCASSERT(lane < workgroup.size(), lane, workgroup.size());
        ThreadState &thread = workgroup[lane];
        RDCASSERT(!thread.Finished());
        activeMask[lane] = true;
        anyActiveThreads = true;
      }

      // step all threads in the tangle
      for(const ThreadReference &ref : threadRefs)
      {
        const uint32_t threadId = ref.id;
        const uint32_t lane = threadId;

        ThreadState &thread = workgroup[lane];
        if(thread.nextInstruction >= instructionOffsets.size())
        {
          if(lane == activeLaneIndex)
            ret.emplace_back();
          continue;
        }
        RDCASSERTEQUAL(thread.activeMask.size(), activeMask.size());
        memcpy(thread.activeMask.data(), activeMask.data(), activeMask.size() * sizeof(bool));
        QueueJob(lane);
      }
    }

    do
    {
      ProcessQueuedDebugMessages();
      ProcessQueuedDeviceThreadSteps();
      // Convert the simulation threads queued operations into pending operations i.e. GPU commands
      ProcessQueuedOps();
      // Sync any pending GPU operations and set the results to the pending threads
      SyncPendingLanes();

      allStepsCompleted = true;
      for(const Tangle &tangle : tangles)
      {
        if(!tangle.IsAliveActive())
          continue;

        bool tangleStepsCompleted = true;
        const rdcarray<ThreadReference> &threadRefs = tangle.GetThreadRefs();
        for(const ThreadReference &ref : threadRefs)
        {
          const uint32_t threadId = ref.id;
          const uint32_t lane = threadId;
          ThreadState &thread = workgroup[lane];
          if(thread.IsSimulationStepActive())
          {
            tangleStepsCompleted = false;
            break;
          }
        }
        if(!tangleStepsCompleted)
        {
          allStepsCompleted = false;
          break;
        }
      }
    } while(!allStepsCompleted);

    for(Tangle &tangle : tangles)
    {
      if(!tangle.IsAliveActive())
        continue;

      const rdcarray<ThreadReference> &threadRefs = tangle.GetThreadRefs();
#if ENABLED(RDOC_DEVEL)
      for(const ThreadReference &ref : threadRefs)
      {
        const uint32_t threadId = ref.id;
        const uint32_t lane = threadId;
        ThreadState &thread = workgroup[lane];
        RDCASSERT(!thread.IsSimulationStepActive());
      }
#endif    // #if ENABLED(RDOC_DEVEL)

      ExecutionPoint newConvergeInstruction = INVALID_EXECUTION_POINT;
      ExecutionPoint newFunctionReturnPoint = INVALID_EXECUTION_POINT;
      uint32_t countActiveThreads = 0;
      uint32_t countDivergedThreads = 0;
      uint32_t countIdenticalConvergePointThreads = 0;
      uint32_t countFunctionReturnThreads = 0;

      // Update the control flow state
      for(const ThreadReference &ref : threadRefs)
      {
        const uint32_t threadId = ref.id;
        const uint32_t lane = threadId;
        ThreadState &thread = workgroup[lane];

        if(thread.nextInstruction >= instructionOffsets.size())
        {
          tangle.SetThreadDead(threadId);
          continue;
        }
        bool wasActive = !thread.Finished();

        threadExecutionStates[threadId] = thread.GetEnteredPoints();

        uint32_t threadConvergeInstruction = thread.GetConvergenceInstruction();
        tangle.SetThreadMergePoint(threadId, threadConvergeInstruction);
        // the thread activated a new convergence point
        if(threadConvergeInstruction != INVALID_EXECUTION_POINT)
        {
          wasActive = true;
          if(newConvergeInstruction == INVALID_EXECUTION_POINT)
          {
            newConvergeInstruction = threadConvergeInstruction;
            RDCASSERTNOTEQUAL(newConvergeInstruction, INVALID_EXECUTION_POINT);
          }
          if(newConvergeInstruction == threadConvergeInstruction)
            ++countIdenticalConvergePointThreads;
        }

        uint32_t threadFunctionReturnPoint = thread.GetFunctionReturnPoint();
        // the thread activated a new function return point
        if(threadFunctionReturnPoint != INVALID_EXECUTION_POINT)
        {
          wasActive = true;
          if(newFunctionReturnPoint == INVALID_EXECUTION_POINT)
          {
            newFunctionReturnPoint = threadFunctionReturnPoint;
            RDCASSERTNOTEQUAL(newFunctionReturnPoint, INVALID_EXECUTION_POINT);
          }
          else
          {
            // All the threads in the tangle should set the same function return point
            RDCASSERTEQUAL(threadFunctionReturnPoint, newFunctionReturnPoint);
          }
          ++countFunctionReturnThreads;
        }

        if(thread.IsDiverged())
        {
          wasActive = true;
          ++countDivergedThreads;
        }

        if(thread.Finished())
          tangle.SetThreadDead(threadId);

        countActiveThreads += wasActive ? 1 : 0;
      }

      for(const ThreadReference &ref : threadRefs)
      {
        const uint32_t threadId = ref.id;
        const uint32_t lane = threadId;
        workgroup[lane].currentInstruction = workgroup[lane].nextInstruction;
      }

      // If the tangle has a common merge point set it here (this will clear the thread merge point)
      // otherwise the convergence point will come from the threads during control flow divergence processing
      if(countIdenticalConvergePointThreads == countActiveThreads)
        tangle.AddMergePoint(newConvergeInstruction);

      if(countFunctionReturnThreads)
      {
        // all the active threads should have a function return point if any have one
        RDCASSERTEQUAL(countFunctionReturnThreads, countActiveThreads);
        tangle.AddFunctionReturnPoint(newFunctionReturnPoint);
      }
      if(countDivergedThreads)
      {
        // all the active threads should have diverged if any diverges
        RDCASSERTEQUAL(countDivergedThreads, countActiveThreads);
        tangle.SetDiverged(true);
      }
    }
    if(!anyActiveThreads)
    {
      active.dead = true;
      controlFlow.UpdateState(threadExecutionStates);
      RDCERR("No active threads in any tangle, killing active thread to terminate the debugger");
    }
    controlFlow.UpdateState(threadExecutionStates);
  }

  RDCASSERT(allStepsCompleted);
  shaderChangesReturn = NULL;
  return ret;
}

ShaderVariable Debugger::MakeTypedPointer(uint64_t value, const DataType &type) const
{
  rdcspv::Id typeId = type.InnerType();
  ShaderVariable var;
  var.rows = var.columns = 1;
  var.type = VarType::GPUPointer;
  var.SetTypedPointer(value, apiWrapper ? apiWrapper->GetShaderID() : ResourceId(),
                      idToPointerType[typeId]);

  const Decorations &dec = decorations[type.id];
  if(dec.flags & Decorations::HasArrayStride)
  {
    uint32_t arrayStride = dec.arrayStride;
    setArrayStride(var, arrayStride);
  }
  return var;
}

ShaderVariable Debugger::MakePointerVariable(Id id, const ShaderVariable *v, uint8_t scalar0,
                                             uint8_t scalar1) const
{
  ShaderVariable var;
  var.rows = var.columns = 1;
  var.type = VarType::GPUPointer;
  var.name = GetRawName(id);
  var.SetTypedPointer(0, ResourceId(), OpaquePointerTypeID);
  setPointer(var, v);
  setScalars(var, scalar0, scalar1);
  setBaseId(var, id);
  return var;
}

ShaderVariable Debugger::MakeCompositePointer(const ShaderVariable &base, Id id,
                                              rdcarray<uint32_t> &indices) const
{
  const ShaderVariable *leaf = &base;

  bool physicalPointer = IsPhysicalPointer(base);
  bool isArray = false;

  if(!physicalPointer)
  {
    // if the base is a plain value, we just start walking down the chain. If the base is a pointer
    // though, we want to step down the chain in the underlying storage, so dereference first.
    if(base.type == VarType::GPUPointer)
      leaf = getPointer(base);
  }

  // if this is an arrayed opaque binding, the first index is a 'virtual' array index into the
  // binding.
  // We only take this if this is the FIRST dereference from the global pointer.
  // If the SPIR-V does something like structType *_1234 =
  if((leaf->type == VarType::ReadWriteResource || leaf->type == VarType::ReadOnlyResource ||
      leaf->type == VarType::Sampler) &&
     checkPointerFlags(*leaf, PointerFlags::GlobalArrayBinding) &&
     getBufferTypeId(base) == rdcspv::Id())
  {
    isArray = true;
  }

  if((leaf->type == VarType::ReadWriteResource && checkPointerFlags(*leaf, PointerFlags::SSBO)) ||
     physicalPointer)
  {
    ShaderVariable ret;
    uint64_t byteOffset = 0;
    const DataType *type = NULL;

    if(physicalPointer)
    {
      // work purely with the pointer itself. All we're going to do effectively is move the address
      // and set the sub-type pointed to so that we know how to dereference it later
      ret = *leaf;
      // if this hasn't been dereferenced yet we should have a valid pointer type ID for a physical
      // pointer, which we can then use to get the base ID (and there will be no buffer type ID
      // below). If not, we rely on the buffer type ID.
      if(!checkPointerFlags(ret, PointerFlags::DereferencedPhysical))
      {
        rdcspv::Id typeId = pointerTypeToId[ret.GetPointer().pointerTypeID];
        RDCASSERT(typeId != rdcspv::Id());
        type = &dataTypes[typeId];
      }
    }
    else
    {
      ret = MakePointerVariable(id, leaf);

      byteOffset = getByteOffset(base);
      type = &dataTypes[idTypes[id]];

      RDCASSERT(type->type == DataType::PointerType);
      type = &dataTypes[type->InnerType()];
    }

    setMatrixStride(ret, getMatrixStride(base));
    setPointerFlags(ret, getPointerFlags(base));

    rdcspv::Id typeId = getBufferTypeId(base);

    if(typeId != rdcspv::Id())
      type = &dataTypes[typeId];

    // first walk any aggregate types
    size_t i = 0;

    // if it's an array, consume the array index first
    if(isArray)
    {
      setBindArrayIndex(ret, indices[i++]);
      type = &dataTypes[type->InnerType()];
    }
    else
    {
      setBindArrayIndex(ret, getBindArrayIndex(base));
    }

    Decorations curDecorations = decorations[type->id];

    uint32_t arrayStride = 0;

    while(i < indices.size() &&
          (type->type == DataType::ArrayType || type->type == DataType::StructType))
    {
      if(type->type == DataType::ArrayType)
      {
        // look up the array stride
        const Decorations &dec = decorations[type->id];
        RDCASSERT(dec.flags & Decorations::HasArrayStride);

        // offset increases by index * arrayStride
        arrayStride = dec.arrayStride;
        byteOffset += indices[i] * arrayStride;

        // new type is the inner type
        type = &dataTypes[type->InnerType()];
      }
      else
      {
        // otherwise it's a struct member
        const DataType::Child &child = type->children[indices[i]];

        // offset increases by member offset
        RDCASSERT(child.decorations.flags & Decorations::HasOffset);
        byteOffset += child.decorations.offset;

        // new type is the child type
        type = &dataTypes[child.type];
        curDecorations = child.decorations;
      }
      i++;
    }

    size_t remaining = indices.size() - i;
    if(remaining == 2)
    {
      // pointer to a scalar in a matrix. indices[i] is column, indices[i + 1] is row
      RDCASSERT(curDecorations.flags & Decorations::HasMatrixStride);

      // type is the resulting scalar (first inner does matrix->colun type, second does column
      // type->scalar type)
      type = &dataTypes[dataTypes[type->InnerType()].InnerType()];

      if(curDecorations.flags & Decorations::RowMajor)
      {
        byteOffset +=
            curDecorations.matrixStride * indices[i + 1] + indices[i] * (type->scalar().width / 8);
      }
      else
      {
        byteOffset +=
            curDecorations.matrixStride * indices[i] + indices[i + 1] * (type->scalar().width / 8);
      }
    }
    else if(remaining == 1)
    {
      if(type->type == DataType::VectorType)
      {
        // pointer to a scalar in a vector.

        // type is the resulting scalar (first inner does matrix->colun type, second does column
        // type->scalar type)
        type = &dataTypes[type->InnerType()];

        byteOffset += indices[i] * (type->scalar().width / 8);
      }
      else
      {
        // pointer to a column in a matrix
        RDCASSERT(curDecorations.flags & Decorations::HasMatrixStride);

        // type is the resulting vector
        type = &dataTypes[type->InnerType()];

        if(curDecorations.flags & Decorations::RowMajor)
        {
          byteOffset += indices[i] * (type->scalar().width / 8);
        }
        else
        {
          byteOffset += curDecorations.matrixStride * indices[i];
        }
      }
    }

    if(curDecorations.flags & Decorations::HasMatrixStride)
      setMatrixStride(ret, curDecorations.matrixStride);

    if(curDecorations.flags & Decorations::RowMajor)
      enablePointerFlags(ret, PointerFlags::RowMajorMatrix);
    else if(curDecorations.flags & Decorations::ColMajor)
      disablePointerFlags(ret, PointerFlags::RowMajorMatrix);

    setBufferTypeId(ret, type->id);
    setArrayStride(ret, arrayStride);
    if(physicalPointer)
    {
      PointerVal ptrval = ret.GetPointer();
      // we use the opaque type ID to ensure we don't accidentally leak the wrong type ID.
      // we check where the pointer is dereferenced to use the physical address instead of the inner
      // binding
      ret.SetTypedPointer(ptrval.pointer + byteOffset, ptrval.shader, OpaquePointerTypeID);
    }
    else
    {
      setByteOffset(ret, byteOffset);
    }
    // this flag is only used for physical pointers, to indicate that it's been dereferenced and
    // the pointer type should be fetched from our ID above and it returned as a plain value, rather
    // than showing the pointer 'natively'. This is because we may not have a pointer type to
    // reference if e.g. the only pointer type registered is struct foo { } and we've dereferenced
    // into inner struct bar { }
    //
    // effectively physical pointers currently decay into opaque pointers after any access chain
    // (but opaque that still uses an address, not that uses a global pointer inner value as in
    // other opaque pointers)
    if(physicalPointer)
      enablePointerFlags(ret, PointerFlags::DereferencedPhysical);
    return ret;
  }

  // first walk any struct member/array indices
  size_t i = 0;
  if(isArray)
    i++;
  while(i < indices.size() && !leaf->members.empty())
  {
    uint32_t idx = indices[i++];
    if(idx >= leaf->members.size())
    {
      AddDebugMessage(MessageCategory::Execution, MessageSeverity::High,
                      MessageSource::RuntimeWarning,
                      StringFormat::Fmt("Index %u invalid at leaf %s. Clamping to %zu", idx,
                                        leaf->name.c_str(), leaf->members.size() - 1));
      idx = uint32_t(leaf->members.size() - 1);
    }
    leaf = &leaf->members[idx];
  }

  // apply any remaining scalar selectors
  uint8_t scalar0 = 0xff, scalar1 = 0xff;

  size_t remaining = indices.size() - i;

  if(remaining > 2)
  {
    AddDebugMessage(
        MessageCategory::Execution, MessageSeverity::High, MessageSource::RuntimeWarning,
        StringFormat::Fmt("Too many indices left (%zu) at leaf %s. Ignoring all but last two",
                          remaining, leaf->name.c_str()));
    i = indices.size() - 2;
  }

  if(remaining == 2)
  {
    scalar0 = indices[i] & 0xff;
    scalar1 = indices[i + 1] & 0xff;
  }
  else if(remaining == 1)
  {
    scalar0 = indices[i] & 0xff;
  }

  ShaderVariable ret = MakePointerVariable(id, leaf, scalar0, scalar1);

  if(isArray)
    setBindArrayIndex(ret, indices[0]);

  return ret;
}

uint64_t Debugger::GetPointerByteOffset(const ShaderVariable &ptr) const
{
  return getByteOffset(ptr);
}

DebugAPIWrapper::TextureType Debugger::GetTextureType(const ShaderVariable &img) const
{
  return getTextureType(img);
}

DeviceOpResult Debugger::GetPointerValue(const ShaderVariable &ptr, ShaderVariable &ret) const
{
  // opaque pointers display as their inner value
  if(IsOpaquePointer(ptr))
  {
    const ShaderVariable *inner = getPointer(ptr);
    ret = *inner;
    ret.name = ptr.name;
    // inherit any array index from the pointer
    ShaderBindIndex bind = ret.GetBindIndex();
    bind.arrayElement = getBindArrayIndex(ptr);
    ret.SetBindIndex(bind);
    return DeviceOpResult::Succeeded;
  }
  // physical pointers which haven't been dereferenced are returned as-is, they're ready for display
  else if(IsPhysicalPointer(ptr) && !checkPointerFlags(ptr, PointerFlags::DereferencedPhysical))
  {
    ret = ptr;
    return DeviceOpResult::Succeeded;
  }

  // every other kind of pointer displays as its contents
  return ReadFromPointer(ptr, ret);
}

DeviceOpResult Debugger::ReadFromPointer(const ShaderVariable &ptr, ShaderVariable &ret) const
{
  if(ptr.type != VarType::GPUPointer)
  {
    ret = ptr;
    return DeviceOpResult::Succeeded;
  }

  if(isUndefPointer(ptr))
  {
    ret = ShaderVariable(ptr.name, 0, 0, 0, 0);
    memset(&ret.value, 0xcc, sizeof(ret.value));
    return DeviceOpResult::Succeeded;
  }

  // values for setting up pointer reads, either from a physical pointer or from an opaque pointer
  rdcspv::Id typeId;
  Decorations parentDecorations;
  uint64_t baseAddress;
  ShaderBindIndex bind;
  uint64_t byteOffset = 0;
  std::function<void(uint64_t offset, uint64_t size, void *dst)> pointerReadCallback;
  if(IsPhysicalPointer(ptr))
  {
    baseAddress = ptr.GetPointer().pointer;
    if(!IsDeviceThread() && !apiWrapper->IsBufferCached(baseAddress))
      return DeviceOpResult::NeedsDevice;

    if(checkPointerFlags(ptr, PointerFlags::DereferencedPhysical))
      typeId = getBufferTypeId(ptr);
    else
      typeId = pointerTypeToId[ptr.GetPointer().pointerTypeID];

    RDCASSERT(typeId != rdcspv::Id());

    parentDecorations = decorations[typeId];
    uint32_t varMatrixStride = getMatrixStride(ptr);

    if(varMatrixStride != 0)
    {
      if(checkPointerFlags(ptr, PointerFlags::RowMajorMatrix))
        parentDecorations.flags = Decorations::RowMajor;
      else
        parentDecorations.flags = Decorations::ColMajor;
      parentDecorations.flags =
          Decorations::Flags(parentDecorations.flags | Decorations::HasMatrixStride);
      parentDecorations.matrixStride = varMatrixStride;
    }
    pointerReadCallback = [this, baseAddress](uint64_t offset, uint64_t size, void *dst) {
      apiWrapper->ReadAddress(baseAddress + offset, size, dst);
    };
  }
  else
  {
    const ShaderVariable *inner = getPointer(ptr);
    if(inner == NULL)
    {
      ret = ShaderVariable(ptr.name, 0, 0, 0, 0);
      return DeviceOpResult::Succeeded;
    }
    if(inner->type == VarType::ReadWriteResource && checkPointerFlags(*inner, PointerFlags::SSBO))
    {
      typeId = getBufferTypeId(ptr);
      byteOffset = getByteOffset(ptr);
      bind = inner->GetBindIndex();
      bind.arrayElement = getBindArrayIndex(ptr);
      if(!IsDeviceThread() && !apiWrapper->IsBufferCached(bind))
        return DeviceOpResult::NeedsDevice;

      uint32_t varMatrixStride = getMatrixStride(ptr);
      if(varMatrixStride != 0)
      {
        if(checkPointerFlags(ptr, PointerFlags::RowMajorMatrix))
          parentDecorations.flags = Decorations::RowMajor;
        else
          parentDecorations.flags = Decorations::ColMajor;
        parentDecorations.flags =
            Decorations::Flags(parentDecorations.flags | Decorations::HasMatrixStride);
        parentDecorations.matrixStride = varMatrixStride;
      }
      pointerReadCallback = [this, bind](uint64_t offset, uint64_t size, void *dst) {
        apiWrapper->ReadBufferValue(bind, offset, size, dst);
      };
    }
  }

  if(pointerReadCallback)
  {
    auto readCallback = [this, pointerReadCallback](ShaderVariable &var, const Decorations &dec,
                                                    const DataType &type, uint64_t offset,
                                                    const rdcstr &) {
      // ignore any callbacks we get on the way up for structs/arrays, we don't need it we only read
      // or write at primitive level
      if(!var.members.empty())
        return;

      bool rowMajor = (dec.flags & Decorations::RowMajor) != 0;
      uint32_t matrixStride = dec.matrixStride;

      if(type.type == DataType::MatrixType)
      {
        RDCASSERT(matrixStride != 0);

        if(rowMajor)
        {
          for(uint8_t r = 0; r < var.rows; r++)
          {
            pointerReadCallback(offset + r * matrixStride, VarTypeByteSize(var.type) * var.columns,
                                VarElemPointer(var, r * var.columns));
          }
        }
        else
        {
          ShaderVariable tmp;
          tmp.type = var.type;

          // read column-wise
          for(uint8_t c = 0; c < var.columns; c++)
          {
            pointerReadCallback(offset + c * matrixStride, VarTypeByteSize(var.type) * var.rows,
                                VarElemPointer(tmp, c * var.rows));
          }

          // transpose into our row major storage
          for(uint8_t r = 0; r < var.rows; r++)
            for(uint8_t c = 0; c < var.columns; c++)
              copyComp(var, r * var.columns + c, tmp, c * var.rows + r);
        }
      }
      else if(type.type == DataType::VectorType)
      {
        if(!rowMajor)
        {
          // we can read a vector at a time if the matrix is column major
          pointerReadCallback(offset, VarTypeByteSize(var.type) * var.columns,
                              VarElemPointer(var, 0));
        }
        else
        {
          for(uint8_t c = 0; c < var.columns; c++)
          {
            pointerReadCallback(offset + c * matrixStride, VarTypeByteSize(var.type),
                                VarElemPointer(var, c));
          }
        }
      }
      else if(type.type == DataType::ScalarType || type.type == DataType::PointerType)
      {
        pointerReadCallback(offset, VarTypeByteSize(var.type), VarElemPointer(var, 0));
        if(type.type == DataType::PointerType)
        {
          auto it = idToPointerType.find(type.InnerType());
          if(it != idToPointerType.end())
          {
            var.SetTypedPointer(var.value.u64v[0], this->apiWrapper->GetShaderID(), it->second);
          }
          else
          {
            var.SetTypedPointer(var.value.u64v[0], ResourceId(), OpaquePointerTypeID);
            enablePointerFlags(var, PointerFlags::DereferencedPhysical);
            setMatrixStride(var, matrixStride);
            setBufferTypeId(var, type.InnerType());
          }
        }
      }
    };

    WalkVariable<ShaderVariable, true>(parentDecorations, dataTypes[typeId], byteOffset, false, ret,
                                       rdcstr(), readCallback);

    ret.name = ptr.name;
    return DeviceOpResult::Succeeded;
  }

  // this is the case of 'reading' from a pointer where the data is entirely contained within the
  // inner pointed variable. Either opaque sampler/image etc which is just the binding, or a
  // cbuffer pointer which was already evaluated
  const ShaderVariable *inner = getPointer(ptr);

  ret = *inner;
  ret.name = ptr.name;

  if(inner->type == VarType::ReadOnlyResource || inner->type == VarType::ReadWriteResource ||
     inner->type == VarType::Sampler)
  {
    bind = ret.GetBindIndex();
    bind.arrayElement = getBindArrayIndex(ptr);
    ret.SetBindIndex(bind);
  }

  // we don't support pointers to scalars since our 'unit' of pointer is a ShaderVariable, so check
  // if we have scalar indices to apply:
  uint8_t scalar0 = 0, scalar1 = 0;
  rdctie(scalar0, scalar1) = getScalars(ptr);

  ShaderVariable tmp = ret;

  if(ret.rows > 1)
  {
    // matrix case
    ClampScalars(ret, scalar0, scalar1);

    if(scalar0 != 0xff && scalar1 != 0xff)
    {
      // two indices - selecting a scalar. scalar0 is the first index in the chain so it chooses
      // column
      copyComp(ret, 0, tmp, scalar1 * ret.columns + scalar0);

      // it's a scalar now, even if it was a matrix before
      ret.rows = ret.columns = 1;
    }
    else if(scalar0 != 0xff)
    {
      // one index, selecting a column
      for(uint32_t row = 0; row < ret.rows; row++)
        copyComp(ret, row, tmp, row * ret.columns + scalar0);

      // it's a vector now, even if it was a matrix before.
      // since we have the convention of row vectors in RenderDoc, adjust the size too
      ret.columns = ret.rows;
      ret.rows = 1;
    }
  }
  else
  {
    ClampScalars(ret, scalar0);

    // vector case, selecting a scalar (if anything)
    if(scalar0 != 0xff)
    {
      copyComp(ret, 0, tmp, scalar0);

      // it's a scalar now, even if it was a matrix before
      ret.columns = 1;
    }
  }

  return DeviceOpResult::Succeeded;
}

Id Debugger::GetPointerBaseId(const ShaderVariable &ptr) const
{
  RDCASSERT(ptr.type == VarType::GPUPointer);

  // we stored the base ID so that it's always available regardless of access chains
  return getBaseId(ptr);
}

uint32_t Debugger::GetPointerArrayStride(const ShaderVariable &ptr) const
{
  RDCASSERT(ptr.type == VarType::GPUPointer);
  return getArrayStride(ptr);
}

bool Debugger::IsOpaquePointer(const ShaderVariable &ptr) const
{
  if(ptr.type != VarType::GPUPointer)
    return false;

  if(IsPhysicalPointer(ptr))
    return false;

  const ShaderVariable *inner = getPointer(ptr);
  return inner->type == VarType::ReadOnlyResource || inner->type == VarType::Sampler ||
         inner->type == VarType::ReadWriteResource;
}

bool Debugger::IsPhysicalPointer(const ShaderVariable &ptr) const
{
  if(ptr.type == VarType::GPUPointer)
  {
    // non-dereferenced physical pointer
    if(ptr.GetPointer().pointerTypeID != OpaquePointerTypeID)
      return true;
    // dereferenced physical pointer
    if(checkPointerFlags(ptr, PointerFlags::DereferencedPhysical))
      return true;
  }
  return false;
}

bool Debugger::ArePointersAndEqual(const ShaderVariable &a, const ShaderVariable &b) const
{
  // we can do a pointer comparison by checking the values, since we store all pointer-related
  // data in there
  if(a.type == VarType::GPUPointer && b.type == VarType::GPUPointer)
    return memcmp(&a.value, &b.value, sizeof(ShaderValue)) == 0;

  return false;
}

DeviceOpResult Debugger::WriteThroughPointer(ShaderVariable &ptr, const ShaderVariable &val) const
{
  // values for setting up pointer reads, either from a physical pointer or from an opaque pointer
  rdcspv::Id typeId;
  Decorations parentDecorations;
  uint64_t baseAddress;
  ShaderBindIndex bind;
  uint64_t byteOffset = 0;
  std::function<void(uint64_t offset, uint64_t size, const void *src)> pointerWriteCallback;

  if(IsPhysicalPointer(ptr))
  {
    baseAddress = ptr.GetPointer().pointer;
    if(!IsDeviceThread() && !apiWrapper->IsBufferCached(baseAddress))
      return DeviceOpResult::NeedsDevice;

    if(checkPointerFlags(ptr, PointerFlags::DereferencedPhysical))
      typeId = getBufferTypeId(ptr);
    else
      typeId = pointerTypeToId[ptr.GetPointer().pointerTypeID];

    RDCASSERT(typeId != rdcspv::Id());
    parentDecorations = decorations[typeId];
    uint32_t varMatrixStride = getMatrixStride(ptr);
    if(varMatrixStride != 0)
    {
      if(checkPointerFlags(ptr, PointerFlags::RowMajorMatrix))
        parentDecorations.flags = Decorations::RowMajor;
      else
        parentDecorations.flags = Decorations::ColMajor;
      parentDecorations.flags =
          Decorations::Flags(parentDecorations.flags | Decorations::HasMatrixStride);
      parentDecorations.matrixStride = varMatrixStride;
    }
    pointerWriteCallback = [this, baseAddress](uint64_t offset, uint64_t size, const void *src) {
      apiWrapper->WriteAddress(baseAddress + offset, size, src);
    };
  }
  else
  {
    const ShaderVariable *inner = getPointer(ptr);
    if(inner->type == VarType::ReadWriteResource && checkPointerFlags(*inner, PointerFlags::SSBO))
    {
      typeId = getBufferTypeId(ptr);
      byteOffset = getByteOffset(ptr);
      bind = inner->GetBindIndex();
      bind.arrayElement = getBindArrayIndex(ptr);
      if(!IsDeviceThread() && !apiWrapper->IsBufferCached(bind))
        return DeviceOpResult::NeedsDevice;

      uint32_t varMatrixStride = getMatrixStride(ptr);
      if(varMatrixStride != 0)
      {
        if(checkPointerFlags(ptr, PointerFlags::RowMajorMatrix))
          parentDecorations.flags = Decorations::RowMajor;
        else
          parentDecorations.flags = Decorations::ColMajor;
        parentDecorations.flags =
            Decorations::Flags(parentDecorations.flags | Decorations::HasMatrixStride);
        parentDecorations.matrixStride = varMatrixStride;
      }
      pointerWriteCallback = [this, bind](uint64_t offset, uint64_t size, const void *src) {
        apiWrapper->WriteBufferValue(bind, offset, size, src);
      };
    }
  }

  if(pointerWriteCallback)
  {
    if(!IsDeviceThread())
      return DeviceOpResult::NeedsDevice;

    auto writeCallback = [pointerWriteCallback](const ShaderVariable &var, const Decorations &dec,
                                                const DataType &type, uint64_t offset,
                                                const rdcstr &) {
      // ignore any callbacks we get on the way up for structs/arrays, we don't need it we only
      // read or write at primitive level
      if(!var.members.empty())
        return;

      bool rowMajor = (dec.flags & Decorations::RowMajor) != 0;
      uint32_t matrixStride = dec.matrixStride;

      if(type.type == DataType::MatrixType)
      {
        RDCASSERT(matrixStride != 0);

        if(rowMajor)
        {
          for(uint8_t r = 0; r < var.rows; r++)
          {
            pointerWriteCallback(offset + r * matrixStride, VarTypeByteSize(var.type) * var.columns,
                                 VarElemPointer(var, r * var.columns));
          }
        }
        else
        {
          ShaderVariable tmp;
          tmp.type = var.type;

          // transpose from our row major storage
          for(uint8_t r = 0; r < var.rows; r++)
            for(uint8_t c = 0; c < var.columns; c++)
              copyComp(tmp, c * var.rows + r, var, r * var.columns + c);

          // write column-wise
          for(uint8_t c = 0; c < var.columns; c++)
          {
            pointerWriteCallback(offset + c * matrixStride, VarTypeByteSize(var.type) * var.rows,
                                 VarElemPointer(tmp, c * var.rows));
          }
        }
      }
      else if(type.type == DataType::VectorType)
      {
        if(!rowMajor)
        {
          // we can write a vector at a time if the matrix is column major
          pointerWriteCallback(offset, VarTypeByteSize(var.type) * var.columns,
                               VarElemPointer(var, 0));
        }
        else
        {
          for(uint8_t c = 0; c < var.columns; c++)
            pointerWriteCallback(offset + c * matrixStride, VarTypeByteSize(var.type),
                                 VarElemPointer(var, c));
        }
      }
      else if(type.type == DataType::ScalarType || type.type == DataType::PointerType)
      {
        pointerWriteCallback(offset, VarTypeByteSize(var.type), VarElemPointer(var, 0));
      }
    };

    WalkVariable<const ShaderVariable, false>(parentDecorations, dataTypes[typeId], byteOffset,
                                              false, val, rdcstr(), writeCallback);

    return DeviceOpResult::Succeeded;
  }

  ShaderVariable *storage = getPointer(ptr);

  // we don't support pointers to scalars since our 'unit' of pointer is a ShaderVariable, so check
  // if we have scalar indices to apply:
  uint8_t scalar0 = 0, scalar1 = 0;
  rdctie(scalar0, scalar1) = getScalars(ptr);

  // in the common case we don't have scalar selectors. In this case just assign the value
  if(scalar0 == 0xff && scalar1 == 0xff)
  {
    AssignValue(*storage, val);
  }
  else
  {
    // otherwise we need to store only the selected part of this pointer. We assume by SPIR-V
    // validity rules that the incoming value matches the pointed value
    if(storage->rows > 1)
    {
      // matrix case
      ClampScalars(*storage, scalar0, scalar1);

      if(scalar0 != 0xff && scalar1 != 0xff)
      {
        // two indices - selecting a scalar. scalar0 is the first index in the chain so it chooses
        // column
        copyComp(*storage, scalar1 * storage->columns + scalar0, val, 0);
      }
      else if(scalar0 != 0xff)
      {
        // one index, selecting a column
        for(uint32_t row = 0; row < storage->rows; row++)
          copyComp(*storage, row * storage->columns + scalar0, val, row);
      }
    }
    else
    {
      ClampScalars(*storage, scalar0);

      // vector case, selecting a scalar
      copyComp(*storage, scalar0, val, 0);
    }
  }
  return DeviceOpResult::Succeeded;
}

rdcstr Debugger::GetHumanName(Id id) const
{
  {
    SCOPED_READLOCK(dynamicNamesLock);
    // see if we have a dynamic name assigned (to disambiguate), if so use that
    auto it = dynamicNames.find(id);
    if(it != dynamicNames.end())
      return it->second;
  }

  // otherwise try the string first
  rdcstr name = strings[id];

  // if we don't have a string name, we can be sure the id is unambiguous
  if(name.empty())
    return GetRawName(id);

  rdcstr basename = name;

  // otherwise check to see if it's been used before. If so give it a new name
  {
    SCOPED_READLOCK(dynamicNamesLock);
    int alias = 2;
    while(usedNames.find(name) != usedNames.end())
    {
      name = basename + "@" + ToStr(alias);
      alias++;
    }
  }

  {
    SCOPED_WRITELOCK(dynamicNamesLock);
    usedNames.insert(name);
    dynamicNames[id] = name;
  }

  return name;
}

void Debugger::AllocateVariable(Id id, Id typeId, ShaderVariable &outVar) const
{
  // allocs should always be pointers
  RDCASSERT(dataTypes[typeId].type == DataType::PointerType);

  auto initCallback = [](ShaderVariable &var, const Decorations &, const DataType &, uint64_t,
                         const rdcstr &) {
    // ignore any callbacks we get on the way up for structs/arrays, we don't need it we only read
    // or write at primitive level
    if(!var.members.empty())
      return;

    // make it obvious when uninitialised values are used
    memset(&var.value, 0xcc, sizeof(var.value));
  };

  WalkVariable<ShaderVariable, true>(Decorations(), dataTypes[dataTypes[typeId].InnerType()], ~0U,
                                     false, outVar, rdcstr(), initCallback);
}

template <typename ShaderVarType, bool allocate>
uint32_t Debugger::WalkVariable(
    const Decorations &curDecorations, const DataType &type, uint64_t offsetOrLocation,
    bool locationUniform, ShaderVarType &var, const rdcstr &accessSuffix,
    std::function<void(ShaderVarType &, const Decorations &, const DataType &, uint64_t, const rdcstr &)>
        callback) const
{
  // if we're walking a const variable we just want to walk it without modification. So outVar
  // is NULL. Otherwise outVar points to the variable itself so we modify it before iterating
  ShaderVariable *outVar = allocate ? pointerIfMutable(var) : NULL;

  // the Location decoration should either be on the variable itself (in which case we hit this
  // first thing), or on the first member of a struct. i.e. once we have a location already and
  // we're auto-assigning from there we shouldn't encounter another location decoration somewhere
  // further down the struct chain. This also prevents us from using the same location for every
  // element in an array, since we have the same set of decorations on the array as on the members
  const bool hasLocation = (curDecorations.flags & Decorations::HasLocation) != 0 || locationUniform;
  if(hasLocation && offsetOrLocation == ~0U)
    offsetOrLocation = curDecorations.location;

  uint32_t numLocations = 0;

  switch(type.type)
  {
    case DataType::ScalarType:
    {
      if(outVar)
      {
        outVar->type = type.scalar().Type();
        outVar->rows = 1;
        outVar->columns = 1;
      }
      numLocations = 1;
      break;
    }
    case DataType::VectorType:
    {
      if(outVar)
      {
        outVar->type = type.scalar().Type();
        outVar->rows = 1U;
        outVar->columns = RDCMAX(1U, type.vector().count) & 0xff;
      }
      numLocations = 1U;
      break;
    }
    case DataType::MatrixType:
    {
      if(outVar)
      {
        outVar->type = type.scalar().Type();
        outVar->columns = RDCMAX(1U, type.matrix().count) & 0xff;
        outVar->rows = RDCMAX(1U, type.vector().count) & 0xff;
      }
      numLocations = var.rows;
      break;
    }
    case DataType::StructType:
    {
      for(int32_t i = 0; i < type.children.count(); i++)
      {
        if(outVar)
        {
          outVar->members.push_back(ShaderVariable());
          if(!type.children[i].name.empty())
            outVar->members.back().name = type.children[i].name;
          else
            outVar->members.back().name = StringFormat::Fmt("_child%d", i);
        }

        rdcstr childAccess = accessSuffix + "." + var.members.back().name;

        const Decorations &childDecorations = type.children[i].decorations;

        uint64_t childOffsetOrLocation = offsetOrLocation;

        // if the struct is concrete, it must have an offset. Otherwise it's opaque and we're using
        // locations
        if(hasLocation)
          childOffsetOrLocation += numLocations;
        else if(childDecorations.flags & Decorations::HasOffset)
          childOffsetOrLocation += childDecorations.offset;

        uint32_t childLocations = WalkVariable<ShaderVarType, allocate>(
            childDecorations, dataTypes[type.children[i].type], childOffsetOrLocation, hasLocation,
            var.members[i], childAccess, callback);

        numLocations += childLocations;
      }
      break;
    }
    case DataType::ArrayType:
    {
      // array stride is decorated on the type, not the member itself
      const Decorations &typeDecorations = decorations[type.id];

      uint32_t childOffset = 0;

      uint32_t len = uintComp(GetActiveLane().ids[type.length], 0);
      for(uint32_t i = 0; i < len; i++)
      {
        if(outVar)
        {
          outVar->members.push_back(ShaderVariable());
          outVar->members.back().name = StringFormat::Fmt("[%u]", i);
        }

        rdcstr childAccess = accessSuffix + var.members.back().name;

        uint32_t childLocations = WalkVariable<ShaderVarType, allocate>(
            curDecorations, dataTypes[type.InnerType()], offsetOrLocation + childOffset,
            hasLocation, var.members[i], childAccess, callback);

        numLocations += childLocations;

        // as above - either the type is concrete and has an array stride, or else we're using
        // locations
        if(hasLocation)
          childOffset = numLocations;
        else if(typeDecorations.flags & Decorations::HasArrayStride)
          childOffset += decorations[type.id].arrayStride;
      }
      break;
    }
    case DataType::PointerType:
    {
      RDCASSERT(dataTypes[type.id].pointerType.storage == StorageClass::PhysicalStorageBuffer);
      if(outVar)
      {
        outVar->type = VarType::GPUPointer;
        outVar->rows = 1;
        outVar->columns = 1;
      }
      numLocations = 1;
      break;
    }
    case DataType::ImageType:
    case DataType::SamplerType:
    case DataType::SampledImageType:
    case DataType::RayQueryType:
    case DataType::AccelerationStructureType:
    case DataType::UnknownType:
    {
      RDCERR("Unexpected variable type %d", type.type);
      return numLocations;
    }
  }

  if(callback)
    callback(var, curDecorations, type, offsetOrLocation, accessSuffix);

  // for auto-assigning locations, we return the number of locations
  return numLocations;
}

bool Debugger::IsDebugExtInstSet(Id id) const
{
  return knownExtSet[ExtSet_ShaderDbg] == id;
}

bool Debugger::InDebugScope(uint32_t inst) const
{
  return m_DebugInfo.lineScope.find(instructionOffsets[inst]) != m_DebugInfo.lineScope.end();
}

const ScopeData *Debugger::GetScope(size_t offset) const
{
  auto it = m_DebugInfo.lineScope.find(offset);
  if(it == m_DebugInfo.lineScope.end())
    return NULL;
  return it->second;
}

void Debugger::PreParse(uint32_t maxId)
{
  Processor::PreParse(maxId);

  strings.resize(idTypes.size());
  idLiveRange.resize(idTypes.size());

  m_InstInfo.reserve(idTypes.size());
}

void Debugger::PostParse()
{
  Processor::PostParse();

  for(std::function<void()> &f : m_DebugInfo.deferredMembers)
    f();
  m_DebugInfo.deferredMembers.clear();

  // declare pointerTypes for all declared physical pointer types. This will match the reflection
  for(auto it = dataTypes.begin(); it != dataTypes.end(); ++it)
  {
    if(it->second.type == DataType::PointerType &&
       it->second.pointerType.storage == rdcspv::StorageClass::PhysicalStorageBuffer)
    {
      idToPointerType.insert(std::make_pair(it->second.InnerType(), (uint16_t)idToPointerType.size()));
    }
  }
  pointerTypeToId.resize(idToPointerType.size());
  for(auto it = idToPointerType.begin(); it != idToPointerType.end(); ++it)
    pointerTypeToId[it->second] = it->first;

  for(const MemberName &mem : memberNames)
    dataTypes[mem.id].children[mem.member].name = mem.name;

  // global IDs never hit a death point
  for(const Variable &v : globals)
    idLiveRange[v.id].second = ~0U;

  if(m_DebugInfo.valid)
  {
    for(auto it = m_DebugInfo.scopes.begin(); it != m_DebugInfo.scopes.end(); ++it)
    {
      ScopeData *scope = &it->second;

      // keep every ID referenced by a local alive until the scope ends. We do this even if a source
      // variable maps to multiple debug variables and technically the earlier ones could be left to
      // die when superceeded by the later ones. This is simple and only means a little bloating of
      // debug variables in the UI (which generally won't be viewed directly anyway)
      for(LocalMapping &m : scope->localMappings)
      {
        Id id = m.debugVar;

        if(id == Id())
          continue;

        idLiveRange[id].second = RDCMAX(scope->end + 1, idLiveRange[id].second);
      }

      // every scope's parent lasts at least as long as it
      while(scope->parent)
      {
        scope->parent->end = RDCMAX(scope->parent->end, scope->end);
        scope = scope->parent;
      }
    }
  }

  memberNames.clear();
}

void Debugger::SetDebugTypeMember(const OpShaderDbg &member, TypeData &resultType, size_t memberIndex)
{
  rdcstr memberName;
  Id memberType;
  uint32_t memberOffset = 0;

  switch(member.inst)
  {
    case ShaderDbg::TypeMember:
      memberName = strings[member.arg<Id>(0)];
      memberType = member.arg<Id>(1);
      memberOffset = EvaluateConstant(member.arg<Id>(5), {}).value.u32v[0];
      break;
    case ShaderDbg::Function:
      memberName = strings[member.arg<Id>(0)];
      memberType = member.arg<Id>(1);
      break;
    case ShaderDbg::TypeInheritance: memberName = "Inheritence"; break;
    default: RDCERR("Unhandled DebugTypeComposite entry %u", member.inst);
  }

  resultType.structMembers[memberIndex] = {memberName, memberType};
  resultType.memberOffsets[memberIndex] = memberOffset;
}

void Debugger::RegisterOp(Iter it)
{
  Processor::RegisterOp(it);

  OpDecoder opdata(it);

  // we add +1 so that we don't remove the ID on its last use, but the next subsequent instruction
  // since blocks always end with a terminator that doesn't consume IDs we're interested in
  // (variables) we'll always have one extra instruction to step to
  OpDecoder::ForEachID(it, [this, &it](Id id, bool result) {
    if(result)
      idLiveRange[id].first = it.offs();
    idLiveRange[id].second = RDCMAX(it.offs() + 1, idLiveRange[id].second);
  });

  bool leaveScope = false;
  bool executable = curFunction != NULL;

  const uint32_t curInstIndex = (uint32_t)instructionOffsets.size();

  if(opdata.op == Op::ExtInst || opdata.op == Op::ExtInstWithForwardRefsKHR)
  {
    OpExtInst extinst(it);

    if(knownExtSet[ExtSet_GLSL450] == extinst.set)
    {
      // all parameters to GLSL.std.450 are Ids, extend idDeathOffset appropriately
      for(const uint32_t param : extinst.params)
      {
        Id id = Id::fromWord(param);
        idLiveRange[id].second = RDCMAX(it.offs() + 1, idLiveRange[id].second);
      }
    }
    else if(knownExtSet[ExtSet_Printf] == extinst.set)
    {
      // all parameters to NonSemantic.DebugPrintf are Ids, extend idDeathOffset appropriately
      for(const uint32_t param : extinst.params)
      {
        Id id = Id::fromWord(param);
        idLiveRange[id].second = RDCMAX(it.offs() + 1, idLiveRange[id].second);
      }
    }
    else if(knownExtSet[ExtSet_ShaderDbg] == extinst.set)
    {
      // the types are identical just with different accessors
      OpShaderDbg &dbg = (OpShaderDbg &)extinst;

      if(dbg.inst != ShaderDbg::Value)
        executable = false;

      switch(dbg.inst)
      {
        case ShaderDbg::Source:
        {
          int32_t fileIndex = (int32_t)m_DebugInfo.sources.size();

          m_DebugInfo.sources[dbg.result] = fileIndex;
          m_DebugInfo.filenames[dbg.result] = strings[dbg.arg<Id>(0)];
          break;
        }
        case ShaderDbg::CompilationUnit:
        {
          m_DebugInfo.scopes[dbg.result] = {
              DebugScope::CompilationUnit,
              NULL,
              1,
              1,
              m_DebugInfo.sources[dbg.arg<Id>(2)],
              0,
              m_DebugInfo.filenames[dbg.arg<Id>(2)],
          };
          break;
        }
        case ShaderDbg::FunctionDefinition:
        {
          m_DebugInfo.funcToDebugFunc[dbg.arg<Id>(1)] = dbg.arg<Id>(0);
          break;
        }
        case ShaderDbg::Function:
        {
          rdcstr name = strings[dbg.arg<Id>(0)];
          // ignore arg 1 type
          // don't use arg 2 source - assume the parent is in the same file so it's redundant
          uint32_t line = EvaluateConstant(dbg.arg<Id>(3), {}).value.u32v[0];
          uint32_t column = EvaluateConstant(dbg.arg<Id>(4), {}).value.u32v[0];
          ScopeData *parent = &m_DebugInfo.scopes[dbg.arg<Id>(5)];
          // ignore arg 6 linkage name
          // ignore arg 7 flags
          // ignore arg 8 scope line
          // ignore arg 9 (optional) declaration

          m_DebugInfo.scopes[dbg.result] = {
              DebugScope::Function, parent, line, column, parent->fileIndex, 0, name,
          };
          break;
        }
        case ShaderDbg::TypeBasic:
        {
          uint32_t byteSize = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          uint32_t encoding = EvaluateConstant(dbg.arg<Id>(2), {}).value.u32v[0];
          switch(encoding)
          {
            case 2: m_DebugInfo.types[dbg.result].type = VarType::Bool; break;
            case 3:
              if(byteSize == 64)
                m_DebugInfo.types[dbg.result].type = VarType::Double;
              else if(byteSize == 32)
                m_DebugInfo.types[dbg.result].type = VarType::Float;
              else if(byteSize == 16)
                m_DebugInfo.types[dbg.result].type = VarType::Half;
              break;
            case 4:
              if(byteSize == 64)
                m_DebugInfo.types[dbg.result].type = VarType::SLong;
              else if(byteSize == 32)
                m_DebugInfo.types[dbg.result].type = VarType::SInt;
              else if(byteSize == 16)
                m_DebugInfo.types[dbg.result].type = VarType::SShort;
              else if(byteSize == 8)
                m_DebugInfo.types[dbg.result].type = VarType::SByte;
              break;
            case 5: m_DebugInfo.types[dbg.result].type = VarType::SByte; break;
            case 6:
              if(byteSize == 64)
                m_DebugInfo.types[dbg.result].type = VarType::ULong;
              else if(byteSize == 32)
                m_DebugInfo.types[dbg.result].type = VarType::UInt;
              else if(byteSize == 16)
                m_DebugInfo.types[dbg.result].type = VarType::UShort;
              else if(byteSize == 8)
                m_DebugInfo.types[dbg.result].type = VarType::UByte;
              break;
            case 7: m_DebugInfo.types[dbg.result].type = VarType::UByte; break;
          }
          break;
        }
        case ShaderDbg::TypePointer:
        {
          m_DebugInfo.types[dbg.result].baseType = dbg.arg<Id>(0);
          m_DebugInfo.types[dbg.result].type = VarType::GPUPointer;
          break;
        }
        case ShaderDbg::TypeVector:
        {
          m_DebugInfo.types[dbg.result].baseType = dbg.arg<Id>(0);
          m_DebugInfo.types[dbg.result].vecSize = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          break;
        }
        case ShaderDbg::TypeMatrix:
        {
          m_DebugInfo.types[dbg.result].baseType = dbg.arg<Id>(0);
          m_DebugInfo.types[dbg.result].matSize = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          m_DebugInfo.types[dbg.result].colMajorMat =
              EvaluateConstant(dbg.arg<Id>(2), {}).value.u32v[0] != 0;
          break;
        }
        case ShaderDbg::TypeArray:
        {
          m_DebugInfo.types[dbg.result].baseType = dbg.arg<Id>(0);
          size_t countDims = dbg.params.size();
          m_DebugInfo.types[dbg.result].arrayDimensions.resize(countDims - 1);
          for(uint32_t i = 1; i < countDims; ++i)
          {
            size_t idx = i - 1;
            m_DebugInfo.types[dbg.result].arrayDimensions[idx] =
                EvaluateConstant(dbg.arg<Id>(i), {}).value.u32v[0];
          }

          break;
        }
        case ShaderDbg::TypeFunction:
        {
          m_DebugInfo.types[dbg.result].type = VarType::Unknown;
          m_DebugInfo.types[dbg.result].baseType = Id();
          m_DebugInfo.types[dbg.result].matSize = 0;
          m_DebugInfo.types[dbg.result].vecSize = 0;
          break;
        }
        case ShaderDbg::TypeTemplate:
        {
          m_DebugInfo.types[dbg.result] = m_DebugInfo.types[dbg.arg<Id>(0)];
          break;
        }
        case ShaderDbg::TypeMember:
        {
          rdcstr name = strings[dbg.arg<Id>(0)];
          Id type = dbg.arg<Id>(1);
          uint32_t offset = EvaluateConstant(dbg.arg<Id>(4), {}).value.u32v[0];
          (void)offset;
          break;
        }
        case ShaderDbg::TypeComposite:
        {
          rdcstr name = strings[dbg.arg<Id>(0)];
          uint32_t tag = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          const rdcstr tagString[3] = {
              "class ",
              "struct ",
              "union ",
          };

          // don't use arg 2 source - assume the parent is in the same file so it's redundant
          uint32_t line = EvaluateConstant(dbg.arg<Id>(3), {}).value.u32v[0];
          uint32_t column = EvaluateConstant(dbg.arg<Id>(4), {}).value.u32v[0];
          ScopeData *parent = &m_DebugInfo.scopes[dbg.arg<Id>(5)];
          // ignore arg 6 linkage name
          // ignore arg 7 size
          // ignore arg 8 flags

          TypeData &resultType = m_DebugInfo.types[dbg.result];
          for(uint32_t i = 9; i < dbg.params.size(); i++)
          {
            resultType.structMembers.push_back({});
            resultType.memberOffsets.push_back(0);
            size_t memberIndex = resultType.structMembers.size() - 1;

            Id memberId = dbg.arg<Id>(i);
            ConstIter memberIt = GetID(memberId);

            if(!memberIt)
            {
              m_DebugInfo.deferredMembers.push_back(
                  [this, resultId = dbg.result, memberIndex, memberId]() {
                    SetDebugTypeMember(OpShaderDbg(GetID(memberId)), m_DebugInfo.types[resultId],
                                       memberIndex);
                  });

              continue;
            }

            SetDebugTypeMember(OpShaderDbg(memberIt), resultType, memberIndex);
          }

          name = tagString[tag % 3] + name;

          m_DebugInfo.scopes[dbg.result] = {
              DebugScope::Composite, parent, line, column, parent->fileIndex, 0, name,
          };
          break;
        }
        case ShaderDbg::LexicalBlock:
        {
          // don't use arg 0 source - assume the parent is in the same file so it's redundant
          uint32_t line = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          uint32_t column = EvaluateConstant(dbg.arg<Id>(2), {}).value.u32v[0];
          ScopeData *parent = &m_DebugInfo.scopes[dbg.arg<Id>(3)];

          rdcstr name;
          if(dbg.params.count() >= 5)
          {
            name = strings[dbg.arg<Id>(4)];
            if(name.isEmpty())
              name = "anonymous_scope";
          }
          else
          {
            name = parent->name + ":" + ToStr(line);
          }

          m_DebugInfo.scopes[dbg.result] = {
              DebugScope::Block, parent, line, column, parent->fileIndex, 0, name,
          };
          break;
        }
        case ShaderDbg::Scope:
        {
          if(m_DebugInfo.curScope)
            m_DebugInfo.curScope->end = it.offs();

          m_DebugInfo.curScope = &m_DebugInfo.scopes[dbg.arg<Id>(0)];

          // pick up any pending mappings for this function if we just entered into a new function.
          // See the comment below in Value for this workaround
          for(size_t i = 0; i < m_DebugInfo.pendingMappings.size(); i++)
          {
            rdcpair<const ScopeData *, LocalMapping> &cur = m_DebugInfo.pendingMappings[i];
            if(m_DebugInfo.curScope->HasAncestor(cur.first))
            {
              m_DebugInfo.curScope->localMappings.push_back(std::move(cur.second));

              // the array isn't sorted so we can just swap the last one into this spot to avoid
              // moving everything
              std::swap(cur, m_DebugInfo.pendingMappings.back());
              m_DebugInfo.pendingMappings.pop_back();
            }
          }

          if(dbg.params.size() >= 2)
            m_DebugInfo.curInline = &m_DebugInfo.inlined[dbg.arg<Id>(1)];
          else
            m_DebugInfo.curInline = NULL;
          break;
        }
        case ShaderDbg::NoScope:
        {
          // don't want to set curScope to NULL until after this instruction. That way flood-fill of
          // scopes in PostParse() can find this instruction in a scope.
          leaveScope = true;
          break;
        }
        case ShaderDbg::GlobalVariable:
        {
          // copy the name string to the variable string only if it's empty. If it has a name
          // already,
          // we prefer that. If the variable is DebugInfoNone then we don't care about it's name.
          if(strings[dbg.arg<Id>(7)].empty())
            strings[dbg.arg<Id>(7)] = strings[dbg.arg<Id>(0)];

          OpVariable var(GetID(dbg.arg<Id>(7)));

          if(var.storageClass == StorageClass::Private ||
             var.storageClass == StorageClass::Workgroup || var.storageClass == StorageClass::Output)
          {
            m_DebugInfo.globals.push_back(var.result);
          }
          break;
        }
        case ShaderDbg::LocalVariable:
        {
          m_DebugInfo.locals[dbg.result] = {
              strings[dbg.arg<Id>(0)],
              &m_DebugInfo.scopes[dbg.arg<Id>(5)],
              &m_DebugInfo.types[dbg.arg<Id>(1)],
          };

          m_DebugInfo.scopes[dbg.arg<Id>(5)].locals.push_back(dbg.result);
          break;
        }
        case ShaderDbg::Declare:
        case ShaderDbg::Value:
        {
          Id sourceVarId = dbg.arg<Id>(0);
          Id debugVarId = dbg.arg<Id>(1);

          // check the function this variable is scoped inside of at declaration time.
          const ScopeData *varDeclScope = m_DebugInfo.locals[sourceVarId].scope;

          // bit of a hack - only process declares/values for variables inside a scope that is
          // within that function. If we see a declare/value in another function we defer it hoping
          // that we will encounter a scope later that's valid for it.
          const bool insideValidScope = m_DebugInfo.curScope->HasAncestor(varDeclScope);

          LocalMapping mapping = {curInstIndex, sourceVarId, debugVarId,
                                  dbg.inst == ShaderDbg::Declare};

          if(constants.find(debugVarId) != constants.end() &&
             !m_DebugInfo.constants.contains(debugVarId))
            m_DebugInfo.constants.push_back(debugVarId);

          mapping.indexes.resize(dbg.params.size() - 3);
          for(uint32_t i = 0; i < mapping.indexes.size(); i++)
            mapping.indexes[i] = EvaluateConstant(dbg.arg<Id>(i + 3), {}).value.u32v[0];

          {
            // don't support expressions, only allow for a single 'deref' which is used for
            // variables to 'deref' into the pointed value
            OpShaderDbg expr(GetID(dbg.arg<Id>(2)));

            for(uint32_t i = 0; i < expr.params.size(); i++)
            {
              OpShaderDbg op(GetID(expr.arg<Id>(i)));

              if(op.params.size() > 1 || EvaluateConstant(op.arg<Id>(0), {}).value.u32v[0] != 0)
              {
                RDCERR("Only deref expressions supported");
              }
            }
          }

          if(insideValidScope)
          {
            m_DebugInfo.curScope->localMappings.push_back(mapping);
          }
          else
          {
            // remove any pending mapping that already exists for this variable, without any way to
            // meaningfully know which one to use when we pick the latter.
            m_DebugInfo.pendingMappings.removeIf(
                [&mapping](const rdcpair<const ScopeData *, LocalMapping> &m) {
                  return mapping.isSourceSupersetOf(m.second);
                });

            m_DebugInfo.pendingMappings.push_back({varDeclScope, mapping});
          }

          break;
        }
        case ShaderDbg::InlinedAt:
        {
          // ignore arg 0 the line number
          ScopeData *scope = &m_DebugInfo.scopes[dbg.arg<Id>(1)];

          if(dbg.params.count() >= 3)
            m_DebugInfo.inlined[dbg.result] = {scope, &m_DebugInfo.inlined[dbg.arg<Id>(2)]};
          else
            m_DebugInfo.inlined[dbg.result] = {scope, NULL};
          break;
        }
        case ShaderDbg::InlinedVariable:
        {
          // TODO handle inlined variables
          break;
        }
        case ShaderDbg::Line:
        {
          m_CurLineCol.lineStart = EvaluateConstant(dbg.arg<Id>(1), {}).value.u32v[0];
          m_CurLineCol.lineEnd = EvaluateConstant(dbg.arg<Id>(2), {}).value.u32v[0];
          if(Shader_Debug_SPIRVUseDebugColumnInformation())
          {
            m_CurLineCol.colStart = EvaluateConstant(dbg.arg<Id>(3), {}).value.u32v[0];
            m_CurLineCol.colEnd = EvaluateConstant(dbg.arg<Id>(4), {}).value.u32v[0];
          }

          // find file index by filename matching, this would be nice to improve as it's brittle
          m_CurLineCol.fileIndex = m_DebugInfo.sources[dbg.arg<Id>(0)];
          break;
        }
        case ShaderDbg::NoLine:
        {
          m_CurLineCol = LineColumnInfo();
          break;
        }
        default: break;
      }
    }
  }
  else if(opdata.op == Op::ExtInstImport)
  {
    OpExtInstImport extimport(it);

    if(extimport.result == knownExtSet[ExtSet_ShaderDbg])
    {
      m_DebugInfo.valid = true;
    }
  }
  else if((opdata.op == Op::AccessChain) || (opdata.op == Op::InBoundsAccessChain))
  {
    OpAccessChain chain(it);
    // Base pointers never retire
    idLiveRange[chain.base].second = ~0U;
  }
  else if((opdata.op == Op::PtrAccessChain) || (opdata.op == Op::InBoundsPtrAccessChain))
  {
    OpPtrAccessChain chain(it);
    // Base pointers never retire
    idLiveRange[chain.base].second = ~0U;
  }

  if(opdata.op == Op::Source)
  {
    OpSource source(it);

    if(!source.source.empty())
    {
      m_Files[source.file] = m_Files.size();
    }
  }
  else if(opdata.op == Op::Line)
  {
    OpLine line(it);

    if(m_DebugInfo.valid)
    {
      // ignore any OpLine when we have proper debug info
    }
    else
    {
      m_CurLineCol.lineStart = line.line;
      m_CurLineCol.lineEnd = line.line;
      m_CurLineCol.colStart = line.column;
      m_CurLineCol.fileIndex = (int32_t)m_Files[line.file];
    }
  }
  else if(opdata.op == Op::NoLine)
  {
    if(!m_DebugInfo.valid)
      m_CurLineCol = LineColumnInfo();
  }
  else if(executable)
  {
    // for debug info, only apply line info if we're in a scope. Otherwise the line info may not
    // apply to this instruction. This means OpPhi's will never be line mapped
    if(m_DebugInfo.valid)
    {
      if(m_DebugInfo.curScope)
        m_InstInfo.push_back({curInstIndex, m_CurLineCol});
      else
        m_InstInfo.push_back({curInstIndex, LineColumnInfo()});
    }
    else
    {
      m_InstInfo.push_back({curInstIndex, m_CurLineCol});
    }
  }

  if(m_DebugInfo.valid)
  {
    m_DebugInfo.lineScope[it.offs()] = m_DebugInfo.curScope;
    m_DebugInfo.lineInline[it.offs()] = m_DebugInfo.curInline;
  }

  // if we're explicitly leaving the scope because of a DebugNoScope, or if we're leaving due to the
  // end of a block then set scope to NULL now.
  if(leaveScope || it.opcode() == Op::Kill || it.opcode() == Op::Unreachable ||
     it.opcode() == Op::Branch || it.opcode() == Op::BranchConditional ||
     it.opcode() == Op::Switch || it.opcode() == Op::Return || it.opcode() == Op::ReturnValue)
  {
    if(m_DebugInfo.curScope)
      m_DebugInfo.curScope->end = it.offs();

    m_DebugInfo.curScope = NULL;
    m_DebugInfo.curInline = NULL;
  }

  if(opdata.op == Op::String)
  {
    OpString string(it);

    strings[string.result] = string.string;
  }
  else if(opdata.op == Op::Name)
  {
    OpName name(it);

    // technically you could name a string - in that case we ignore the name
    if(strings[name.target].empty())
      strings[name.target] = name.name;
  }
  else if(opdata.op == Op::MemberName)
  {
    OpMemberName memberName(it);

    memberNames.push_back({memberName.type, memberName.member, memberName.name});
  }
  else if(opdata.op == Op::EntryPoint)
  {
    OpEntryPoint entryPoint(it);

    entryLookup[ShaderEntryPoint(entryPoint.name, MakeShaderStage(entryPoint.executionModel))] =
        entryPoint.entryPoint;
  }
  else if(opdata.op == Op::Function)
  {
    OpFunction func(it);

    curFunction = &functions[func.result];

    curFunction->begin = it.offs();
  }
  else if(opdata.op == Op::FunctionParameter)
  {
    OpFunctionParameter param(it);

    curFunction->parameters.push_back(param.result);
  }
  else if(opdata.op == Op::Variable)
  {
    OpVariable var(it);

    if(var.storageClass == StorageClass::Function && curFunction)
      curFunction->variables.push_back(var.result);

    // variables are always pointers
    Id varType = dataTypes[var.resultType].InnerType();

    // if we don't have a name for this variable but it's a pointer to a struct that is named then
    // give the variable a name based on the type. This is a common pattern in GLSL for global
    // blocks, and since the variable is how we access commonly we should give it a recognisable
    // name.
    //
    // Don't do this if we have debug info, rely on it purely to give us the right data
    if(strings[var.result].empty() && dataTypes[varType].type == DataType::StructType &&
       !strings[varType].empty() && !m_DebugInfo.valid)
    {
      strings[var.result] = strings[varType] + "_var";
    }
  }
  else if(opdata.op == Op::Label)
  {
    OpLabel lab(it);

    labelInstruction[lab.result] = instructionOffsets.count();
  }

  // everything else inside a function becomes an instruction, including the OpFunction and
  // OpFunctionEnd. We won't actually execute these instructions

  instructionOffsets.push_back(it.offs());

  if(opdata.op == Op::FunctionEnd)
  {
    // allow function parameters and variables to live indefinitely
    for(const Id &id : curFunction->parameters)
      idLiveRange[id].second = ~0U;
    for(const Id &id : curFunction->variables)
      idLiveRange[id].second = ~0U;
    curFunction = NULL;
  }
}

// Can be called from any thread
void Debugger::QueueGpuMathOp(uint32_t lane)
{
  ThreadState &thread = workgroup[lane];
  SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
  SPIRV_DEBUG_RDCASSERT(!queuedGpuMathOps[lane]);
  queuedGpuMathOps[lane] = true;
}

// Can be called from any thread
void Debugger::QueueGpuSampleGatherOp(uint32_t lane)
{
  ThreadState &thread = workgroup[lane];
  SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
  SPIRV_DEBUG_RDCASSERT(!queuedGpuSampleGatherOps[lane]);
  queuedGpuSampleGatherOps[lane] = true;
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::ProcessQueuedOps()
{
  CHECK_DEBUGGER_THREAD();
  ProcessQueuedGpuMathOps();
  ProcessQueuedGpuSampleGatherOps();
  SyncPendingGpuOps();
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::SyncPendingLanes()
{
  CHECK_DEBUGGER_THREAD();
  for(uint32_t lane = 0; lane < pendingLanes.size(); ++lane)
  {
    if(pendingLanes[lane])
    {
      pendingLanes[lane] = false;
      ThreadState &thread = workgroup[lane];
      thread.SetPendingResultReady();
      QueueJob(lane);
    }
  }
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::ProcessQueuedGpuMathOps()
{
  CHECK_DEBUGGER_THREAD();
  for(uint32_t lane = 0; lane < queuedGpuMathOps.size(); ++lane)
  {
    if(queuedGpuMathOps[lane])
    {
      if(!apiWrapper->QueuedOpsHasSpace())
        SyncPendingGpuOps();

      queuedGpuMathOps[lane] = false;
      const GpuMathOperation &mathOp = workgroup[lane].GetQueuedGpuMathOp();

      uint32_t workgroupIndex = mathOp.workgroupIndex;
      if(apiWrapper->QueueCalculateMathOp(mathOp.op, mathOp.paramVars))
      {
        pendingGpuMathsOpsResults.push_back(mathOp.result);
      }
      else
      {
        ShaderVariable &result = *mathOp.result;
        memset(&result.value, 0, sizeof(result.value));
      }

      SPIRV_DEBUG_RDCASSERT(!pendingLanes[workgroupIndex]);
      pendingLanes[workgroupIndex] = true;
    }
  }
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::ProcessQueuedGpuSampleGatherOps()
{
  CHECK_DEBUGGER_THREAD();
  for(uint32_t lane = 0; lane < queuedGpuSampleGatherOps.size(); ++lane)
  {
    if(queuedGpuSampleGatherOps[lane])
    {
      if(!apiWrapper->QueuedOpsHasSpace())
        SyncPendingGpuOps();

      queuedGpuSampleGatherOps[lane] = false;
      const GpuSampleGatherOperation &sampleGatherOp = workgroup[lane].GetQueuedGpuSampleGatherOp();

      uint32_t workgroupIndex = sampleGatherOp.workgroupIndex;
      ThreadState &thread = workgroup[workgroupIndex];
      ShaderVariable &result = *sampleGatherOp.result;
      bool hasResult = false;
      if(!(apiWrapper->QueueSampleGather(
             thread, sampleGatherOp.opcode, sampleGatherOp.texType, sampleGatherOp.imageBind,
             sampleGatherOp.samplerBind, sampleGatherOp.uv, sampleGatherOp.ddxCalc,
             sampleGatherOp.ddyCalc, sampleGatherOp.compare, sampleGatherOp.gatherChannel,
             sampleGatherOp.operands, result, hasResult)))
      {
        // sample failed. Pretend we got 0 columns back
        set0001(result);
        hasResult = true;
      }
      if(!hasResult)
        pendingGpuSampleGatherOpsResults.push_back(sampleGatherOp.result);

      SPIRV_DEBUG_RDCASSERT(!pendingLanes[workgroupIndex]);
      pendingLanes[workgroupIndex] = true;
    }
  }
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::SyncPendingGpuOps()
{
  CHECK_DEBUGGER_THREAD();
  if(pendingGpuMathsOpsResults.empty() && pendingGpuSampleGatherOpsResults.empty())
    return;

  if(!(apiWrapper->GetQueuedResults(pendingGpuMathsOpsResults, pendingGpuSampleGatherOpsResults)))
  {
    RDCERR("GetQueuedResults failed");
    return;
  }
  pendingGpuMathsOpsResults.clear();
  pendingGpuSampleGatherOpsResults.clear();
}

// Must be called from the replay manager thread (the debugger thread)
DebugAPIWrapper *Debugger::GetAPIWrapper() const
{
  CHECK_DEBUGGER_THREAD();
  return apiWrapper;
}

void Debugger::SimulationJobHelper()
{
  while(AtomicLoad(&atomic_simulationFinished) == 0)
  {
    for(uint32_t lane = 0; lane < workgroup.size(); ++lane)
    {
      if(Atomic::CmpExch32(&queuedJobs[lane], 1, 0) == 1)
      {
        StepThread(lane, StepThreadMode::RUN_MULTIPLE_STEPS);
      }
    }
  };
}

// Called from any thread
void Debugger::StepThread(uint32_t lane, StepThreadMode stepMode)
{
  ThreadState &thread = workgroup[lane];
  bool isActiveThread = lane == activeLaneIndex;
  bool simulateStep = true;
  SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
  int curActiveSteps = isActiveThread ? steps : 0;

  while(simulateStep)
  {
    simulateStep = false;
    {
      thread.ClearPendingDebugState();
      if(isActiveThread)
        activeDebugState.stepIndex = curActiveSteps;
      InternalStepThread(lane);
      thread.ClearPendingDebugState();
    }
    if(thread.StepNeedsGpuSampleGatherOp())
      break;
    else if(thread.StepNeedsGpuMathOp())
      break;
    else if(thread.StepNeedsDeviceThread())
      break;

    if(isActiveThread)
      curActiveSteps++;

    if(stepMode == StepThreadMode::RUN_SINGLE_STEP)
      break;

    simulateStep = thread.CanRunAnotherStep();
    if(simulateStep)
    {
      SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
    }
    if(simulateStep)
      thread.SetStepQueued();

    if(stepMode == StepThreadMode::QUEUE_MULTIPLE_STEPS)
      break;
  };
  // Update the number of simulation steps
  if(isActiveThread)
    steps = curActiveSteps;

  SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());

  // The queueing has to be when the thread is not being simulated
  if(thread.StepNeedsGpuSampleGatherOp())
  {
    SPIRV_DEBUG_RDCASSERT(!simulateStep);
    QueueGpuSampleGatherOp(lane);
    return;
  }
  if(thread.StepNeedsGpuMathOp())
  {
    SPIRV_DEBUG_RDCASSERT(!simulateStep);
    QueueGpuMathOp(lane);
    return;
  }
  if(thread.StepNeedsDeviceThread())
  {
    SPIRV_DEBUG_RDCASSERT(!simulateStep);
    QueueDeviceThreadStep(lane);
    return;
  }

  if(simulateStep)
  {
    SPIRV_DEBUG_RDCASSERTEQUAL(stepMode, StepThreadMode::QUEUE_MULTIPLE_STEPS);
    QueueJob(lane);
    return;
  }
  SPIRV_DEBUG_RDCASSERT(!thread.IsPendingResultPending());
  thread.SetSimulationStepCompleted();
}

// Called from any thread
void Debugger::InternalStepThread(uint32_t lane)
{
  ThreadState &thread = workgroup[lane];
  if(lane == activeLaneIndex)
  {
    size_t instOffs = instructionOffsets[thread.nextInstruction];

    // see if we're retiring any IDs at this state
    if(retireIDs)
    {
      {
        SPIRV_DEBUG_RDCASSERT(activeDebugState.callstack.empty());
        SPIRV_DEBUG_RDCASSERT(activeDebugState.changes.empty());
        SPIRV_DEBUG_RDCASSERT(activeDebugState.flags == ShaderEvents::NoEvent);
        SPIRV_DEBUG_RDCASSERT(activeDebugState.nextInstruction == 0);
      }
      for(size_t l = 0; l < thread.live.size();)
      {
        Id id = thread.live[l];
        if(idLiveRange[id].second < instOffs)
        {
          thread.live.erase(l);
          ShaderVariableChange change;
          DeviceOpResult opResult = GetPointerValue(thread.ids[id], change.before);
          // The variable was live and written to, it should be cached
          SPIRV_DEBUG_RDCASSERTEQUAL(opResult, DeviceOpResult::Succeeded);
          activeDebugState.changes.push_back(change);
          continue;
        }

        l++;
      }
      retireIDs = false;
    }

    uint32_t funcRet = ~0U;
    size_t prevStackSize = thread.callstack.size();

    if(!thread.callstack.empty())
      funcRet = thread.callstack.back()->funcCallInstruction;

    thread.StepNext(true, activeDebugState.stepIndex, workgroup);
    if(thread.StepNeedsGpuSampleGatherOp())
      return;
    if(thread.StepNeedsGpuMathOp())
      return;
    if(thread.StepNeedsDeviceThread())
      return;

    if(!thread.IsPendingResultPending())
    {
      const ShaderDebugState &pendingDebugState = thread.GetPendingDebugState();
      activeDebugState.nextInstruction = pendingDebugState.nextInstruction;
      activeDebugState.flags = pendingDebugState.flags;
      activeDebugState.changes.append(pendingDebugState.changes);
      thread.ClearPendingDebugState();

      if(thread.callstack.size() > prevStackSize)
        instOffs = instructionOffsets[GetInstructionForFunction(thread.callstack.back()->function)];

      else if(thread.callstack.size() < prevStackSize && funcRet != ~0U)
        instOffs = instructionOffsets[funcRet];

      FillCallstack(thread, activeDebugState);

      if(m_DebugInfo.valid)
      {
        size_t endOffs = instructionOffsets[thread.nextInstruction - 1];

        // append any inlined functions to the top of the stack
        InlineData *inlined = m_DebugInfo.lineInline[endOffs];

        size_t insertPoint = activeDebugState.callstack.size();

        // start with the current scope, it refers to the *inlined* function
        if(inlined)
        {
          const ScopeData *scope = GetScope(endOffs);
          // find the function parent of the current scope
          while(scope && scope->parent && scope->type == DebugScope::Block)
            scope = scope->parent;

          activeDebugState.callstack.insert(insertPoint, scope->name);
        }

        // if this instruction has no scope, don't give it a callstack
        if(GetScope(endOffs) == NULL)
        {
          activeDebugState.callstack.clear();
        }

        // move to the next inline up on our inline stack. If we reach an actual function
        // call, this parent will be NULL as there was no more inlining - the final scope will
        // refer to the real function which is already on our stack
        while(inlined && inlined->parent)
        {
          const ScopeData *scope = inlined->scope;
          // find the function parent of the current scope
          while(scope && scope->parent && scope->type == DebugScope::Block)
            scope = scope->parent;

          activeDebugState.callstack.insert(insertPoint, scope->name);

          inlined = inlined->parent;
        }
      }

      shaderChangesReturn->push_back(activeDebugState);
      {
        activeDebugState.callstack.clear();
        activeDebugState.changes.clear();
        activeDebugState.flags = ShaderEvents::NoEvent;
        activeDebugState.stepIndex = 0;
        activeDebugState.nextInstruction = 0;
        retireIDs = true;
      }
    }
  }
  else
  {
    thread.StepNext(false, ~0U, workgroup);
    if(thread.StepNeedsGpuSampleGatherOp())
      return;
    if(thread.StepNeedsGpuMathOp())
      return;
    if(thread.StepNeedsDeviceThread())
      return;
  }
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::QueueJob(uint32_t lane)
{
  CHECK_DEBUGGER_THREAD();
  ThreadState &thread = workgroup[lane];
  thread.SetStepQueued();
  if(mtSimulation)
  {
    if(Shader_Debug_UseJobSystemJobs())
    {
      Threading::JobSystem::AddJob(
          [this, lane]() { StepThread(lane, StepThreadMode::RUN_MULTIPLE_STEPS); });
    }
    else
    {
      RDCASSERT(Atomic::CmpExch32(&queuedJobs[lane], 0, 1) == 0);
    }
  }
  else
  {
    StepThread(lane, StepThreadMode::RUN_SINGLE_STEP);
  }
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::ProcessQueuedDebugMessages()
{
  rdcarray<DebugMessage> msgs;
  {
    SCOPED_LOCK(queuedDebugMessagesLock);
    queuedDebugMessages.swap(msgs);
  }
  for(const DebugMessage &dbgMsg : msgs)
    apiWrapper->AddDebugMessage(dbgMsg.cat, dbgMsg.sev, dbgMsg.src, dbgMsg.desc);
}

// Called from any thread
void Debugger::AddDebugMessage(MessageCategory c, MessageSeverity sv, MessageSource src, rdcstr d) const
{
  SCOPED_LOCK(queuedDebugMessagesLock);
  queuedDebugMessages.push_back({c, sv, src, d});
}

// Can be called from any thread
void Debugger::QueueDeviceThreadStep(uint32_t lane)
{
  ThreadState &thread = workgroup[lane];
  SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
  thread.SetStepQueued();
  SPIRV_DEBUG_RDCASSERT(!queuedDeviceThreadSteps[lane]);
  queuedDeviceThreadSteps[lane] = true;
}

// Must be called from the replay manager thread (the debugger thread)
void Debugger::ProcessQueuedDeviceThreadSteps()
{
  CHECK_DEBUGGER_THREAD();
  for(uint32_t lane = 0; lane < queuedDeviceThreadSteps.size(); ++lane)
  {
    if(queuedDeviceThreadSteps[lane])
    {
      queuedDeviceThreadSteps[lane] = false;
      ThreadState &thread = workgroup[lane];
      thread.SetPendingResultUnknown();
      SPIRV_DEBUG_RDCASSERT(thread.IsSimulationStepActive());
      StepThread(lane, StepThreadMode::QUEUE_MULTIPLE_STEPS);
    }
  }
}

// Can be called from any thread
void Debugger::FillInputValue(ShaderVariable &var, ShaderBuiltin builtin, uint32_t threadIndex) const
{
  apiWrapper->FillInputValue(var, builtin, threadIndex, 0, 0);
}

DeviceOpResult Debugger::ReadTexel(const ShaderBindIndex &imageBind, const ShaderVariable &coord,
                                   uint32_t sample, ShaderVariable &output) const
{
  if(!IsDeviceThread())
  {
    if(!apiWrapper->IsImageCached(imageBind))
      return DeviceOpResult::NeedsDevice;
  }
  return apiWrapper->ReadTexel(imageBind, coord, sample, output);
}

DeviceOpResult Debugger::WriteTexel(const ShaderBindIndex &imageBind, const ShaderVariable &coord,
                                    uint32_t sample, const ShaderVariable &input) const
{
  if(!IsDeviceThread())
  {
    if(!apiWrapper->IsImageCached(imageBind))
      return DeviceOpResult::NeedsDevice;
  }
  return apiWrapper->WriteTexel(imageBind, coord, sample, input);
}

DeviceOpResult Debugger::GetBufferLength(const ShaderBindIndex &bind, uint64_t &bufferLen) const
{
  if(!IsDeviceThread())
  {
    if(!apiWrapper->IsImageCached(bind))
      return DeviceOpResult::NeedsDevice;
  }
  bufferLen = apiWrapper->GetBufferLength(bind);
  return DeviceOpResult::Succeeded;
}

};    // namespace rdcspv

#if ENABLED(ENABLE_UNIT_TESTS)

#include "catch/catch.hpp"

TEST_CASE("Check SPIRV Id naming", "[tostr]")
{
  SECTION("Test GetRawName")
  {
    CHECK(rdcspv::GetRawName(rdcspv::Id::fromWord(1234)) == "_1234");
    CHECK(rdcspv::GetRawName(rdcspv::Id::fromWord(12345)) == "_12345");
    CHECK(rdcspv::GetRawName(rdcspv::Id::fromWord(999)) == "_999");
    CHECK(rdcspv::GetRawName(rdcspv::Id::fromWord(0xffffffff)) == "_4294967295");
    CHECK(rdcspv::GetRawName(rdcspv::Id()) == "_0");
  };

  SECTION("Test ParseRawName")
  {
    CHECK(rdcspv::ParseRawName("_1234") == rdcspv::Id::fromWord(1234));
    CHECK(rdcspv::ParseRawName("_12345") == rdcspv::Id::fromWord(12345));
    CHECK(rdcspv::ParseRawName("_999") == rdcspv::Id::fromWord(999));
    CHECK(rdcspv::ParseRawName("_4294967295") == rdcspv::Id::fromWord(0xffffffff));
    CHECK(rdcspv::ParseRawName("_0") == rdcspv::Id());
    CHECK(rdcspv::ParseRawName("1234") == rdcspv::Id());
    CHECK(rdcspv::ParseRawName("999") == rdcspv::Id());
    CHECK(rdcspv::ParseRawName("1") == rdcspv::Id());
    CHECK(rdcspv::ParseRawName("-1234") == rdcspv::Id());
    CHECK(rdcspv::ParseRawName("asdf") == rdcspv::Id());
  };
}

#endif
