bool CalculateSampleGather()

in renderdoc/driver/vulkan/vk_shaderdebug.cpp [579:1348]


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

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

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

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

    // if any descriptor lookup failed, return now
    if(!valid)
      return false;

    VkMarkerRegion markerRegion("CalculateSampleGather");

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

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

    // promote view to Array view

    const VulkanCreationInfo::ImageView &viewProps = m_Creation.m_ImageView[GetResID(view)];
    const VulkanCreationInfo::Image &imageProps = m_Creation.m_Image[viewProps.image];

    const bool depthTex = IsDepthOrStencilFormat(viewProps.format);

    VkDevice dev = m_pDriver->GetDev();

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

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

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

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

        RDCEraseEl(output.value);

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

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

        if(buffer)
        {
          const VulkanCreationInfo::BufferView &bufViewProps =
              m_Creation.m_BufferView[GetResID(bufferView)];

          VkDeviceSize size = bufViewProps.size;

          if(size == VK_WHOLE_SIZE)
          {
            const VulkanCreationInfo::Buffer &bufProps = m_Creation.m_Buffer[bufViewProps.buffer];
            size = bufProps.size - bufViewProps.offset;
          }

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

        return true;
      }
      default: break;
    }

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

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

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

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

      m_SampleViews[GetResID(view)] = sampleView;
    }

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

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

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

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

        auto insertIt = m_BiasSamplers.insert(std::make_pair(key, VkSampler()));
        if(insertIt.second)
        {
          const VulkanCreationInfo::Sampler &samplerProps = m_Creation.m_Sampler[key.first];

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

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

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

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

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

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

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

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

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

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

    constParams.operation = (uint32_t)opcode;

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

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

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

        break;
      }
      default: break;
    }

    bool gatherOp = false;

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

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

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

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

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

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

        constParams.gatherChannel = gatherChannel;

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

          constParams.useGradOrGatherOffsets = VK_TRUE;

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

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

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

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

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

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

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

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

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

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

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

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

          constParams.useGradOrGatherOffsets = VK_TRUE;

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

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

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

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

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

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

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

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

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

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

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

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

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

    VkDescriptorBufferInfo uniformWriteInfo = {};
    m_DebugData.ConstantsBuffer.FillDescriptor(uniformWriteInfo);

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

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

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

      rdcarray<VkWriteDescriptorSet> writes;

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

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

      ObjDisp(dev)->UpdateDescriptorSets(Unwrap(dev),
                                         ARRAY_COUNT(m_DebugData.DummyWrites[resetIndex]),
                                         m_DebugData.DummyWrites[resetIndex], 0, NULL);
    }

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

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

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

    m_DebugData.ConstantsBuffer.Unmap();

    {
      VkCommandBuffer cmd = m_pDriver->GetNextCmd();

      if(cmd == VK_NULL_HANDLE)
        return false;

      VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
                                            VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};

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

      VkClearValue clear = {};

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

      ObjDisp(cmd)->CmdBindPipeline(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS, Unwrap(pipe));
      ObjDisp(cmd)->CmdBindDescriptorSets(Unwrap(cmd), VK_PIPELINE_BIND_POINT_GRAPHICS,
                                          Unwrap(m_DebugData.PipeLayout), 0, 1,
                                          UnwrapPtr(m_DebugData.DescSet), 0, NULL);

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

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

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

      VkBufferImageCopy region = {
          0, sizeof(Vec4f), 1, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, {0, 0, 0}, {1, 1, 1},
      };
      ObjDisp(cmd)->CmdCopyImageToBuffer(Unwrap(cmd), Unwrap(m_DebugData.Image),
                                         VK_IMAGE_LAYOUT_GENERAL,
                                         Unwrap(m_DebugData.ReadbackBuffer.buf), 1, &region);

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

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

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

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

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

    uint32_t *retu = (uint32_t *)retf;
    int32_t *reti = (int32_t *)retf;

    // convert full precision results, we did all sampling at 32-bit precision
    for(uint8_t c = 0; c < 4; c++)
    {
      if(VarTypeCompType(output.type) == CompType::Float)
        setFloatComp(output, c, retf[c]);
      else if(VarTypeCompType(output.type) == CompType::SInt)
        setIntComp(output, c, reti[c]);
      else
        setUintComp(output, c, retu[c]);
    }

    m_DebugData.ReadbackBuffer.Unmap();

    return true;
  }