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

#include <float.h>
#include <algorithm>
#include "common/common.h"
#include "data/glsl_shaders.h"
#include "driver/shaders/spirv/glslang_compile.h"
#include "maths/camera.h"
#include "maths/formatpacking.h"
#include "maths/matrix.h"
#include "strings/string_utils.h"
#include "gl_driver.h"
#include "gl_replay.h"
#include "gl_resources.h"

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

void GetGLSLVersions(ShaderType &shaderType, int &glslVersion, int &glslBaseVer, int &glslCSVer)
{
  if(IsGLES)
  {
    shaderType = ShaderType::GLSLES;
    glslVersion = glslBaseVer = glslCSVer = 300;

    if(GLCoreVersion >= 31)
      glslVersion = glslBaseVer = glslCSVer = 310;

    if(GLCoreVersion >= 32)
      glslVersion = glslBaseVer = glslCSVer = 320;
  }
  else
  {
    shaderType = ShaderType::GLSL;
    glslVersion = glslBaseVer = 150;
    glslCSVer = 420;
  }
}

GLuint CreateShader(GLenum shaderType, const rdcstr &src)
{
  GLuint ret = GL.glCreateShader(shaderType);

  const char *csrc = src.c_str();
  GL.glShaderSource(ret, 1, &csrc, NULL);

  GL.glCompileShader(ret);

  char buffer[1024] = {};
  GLint status = 0;
  GL.glGetShaderiv(ret, eGL_COMPILE_STATUS, &status);
  if(status == 0)
  {
    GL.glGetShaderInfoLog(ret, 1024, NULL, buffer);
    RDCERR("%s compile error: %s", ToStr(shaderType).c_str(), buffer);
    return 0;
  }

  return ret;
}

GLuint CreateSPIRVShader(GLenum shaderType, const rdcstr &src)
{
  if(!HasExt[ARB_gl_spirv])
  {
    RDCERR("Compiling SPIR-V shader without ARB_gl_spirv - should be checked above!");
    return 0;
  }

  rdcspv::CompilationSettings settings(rdcspv::InputLanguage::OpenGLGLSL,
                                       rdcspv::ShaderStage(ShaderIdx(shaderType)));

  rdcarray<uint32_t> spirv;
  rdcstr s = rdcspv::Compile(settings, {src}, spirv);

  if(spirv.empty())
  {
    RDCERR("Couldn't compile shader to SPIR-V: %s", s.c_str());
    return 0;
  }

  GLuint ret = GL.glCreateShader(shaderType);

  GL.glShaderBinary(1, &ret, eGL_SHADER_BINARY_FORMAT_SPIR_V, spirv.data(),
                    (GLsizei)spirv.size() * 4);

  GL.glSpecializeShader(ret, "main", 0, NULL, NULL);

  char buffer[1024] = {};
  GLint status = 0;
  GL.glGetShaderiv(ret, eGL_COMPILE_STATUS, &status);
  if(status == 0)
  {
    GL.glGetShaderInfoLog(ret, 1024, NULL, buffer);
    RDCERR("%s compile error: %s", ToStr(shaderType).c_str(), buffer);
    return 0;
  }

  return ret;
}

GLuint CreateCShaderProgram(const rdcstr &src)
{
  GLuint cs = CreateShader(eGL_COMPUTE_SHADER, src);
  if(cs == 0)
    return 0;

  GLuint ret = GL.glCreateProgram();

  GL.glAttachShader(ret, cs);

  GL.glLinkProgram(ret);

  char buffer[1024] = {};
  GLint status = 0;
  GL.glGetProgramiv(ret, eGL_LINK_STATUS, &status);
  if(status == 0)
  {
    GL.glGetProgramInfoLog(ret, 1024, NULL, buffer);
    RDCERR("Link error: %s", buffer);
  }

  GL.glDetachShader(ret, cs);

  GL.glDeleteShader(cs);

  return ret;
}

GLuint CreateShaderProgram(GLuint vs, GLuint fs, GLuint gs)
{
  GLuint ret = GL.glCreateProgram();

  GL.glAttachShader(ret, vs);
  GL.glAttachShader(ret, fs);
  if(gs)
    GL.glAttachShader(ret, gs);

  GL.glLinkProgram(ret);

  char buffer[1024] = {};
  GLint status = 0;
  GL.glGetProgramiv(ret, eGL_LINK_STATUS, &status);
  if(status == 0)
  {
    GL.glGetProgramInfoLog(ret, 1024, NULL, buffer);
    RDCERR("Shader error: %s", buffer);
  }

  return ret;
}

GLuint CreateShaderProgram(const rdcstr &vsSrc, const rdcstr &fsSrc, const rdcstr &gsSrc)
{
  GLuint vs = 0;
  GLuint fs = 0;
  GLuint gs = 0;

  if(vsSrc.empty())
  {
    RDCERR("Must have vertex shader - no separable programs supported.");
    return 0;
  }

  if(fsSrc.empty())
  {
    RDCERR("Must have fragment shader - no separable programs supported.");
    return 0;
  }

  vs = CreateShader(eGL_VERTEX_SHADER, vsSrc);
  if(vs == 0)
    return 0;

  fs = CreateShader(eGL_FRAGMENT_SHADER, fsSrc);
  if(fs == 0)
    return 0;

  if(!gsSrc.empty())
  {
    gs = CreateShader(eGL_GEOMETRY_SHADER, gsSrc);
    if(gs == 0)
      return 0;
  }

  GLuint ret = CreateShaderProgram(vs, fs, gs);

  GL.glDetachShader(ret, vs);
  GL.glDetachShader(ret, fs);
  if(gs)
    GL.glDetachShader(ret, gs);

  GL.glDeleteShader(vs);
  GL.glDeleteShader(fs);
  if(gs)
    GL.glDeleteShader(gs);

  return ret;
}

void GLReplay::CheckGLSLVersion(const char *sl, int &glslVersion)
{
  // GL_SHADING_LANGUAGE_VERSION for OpenGL ES:
  //   "OpenGL ES GLSL ES N.M vendor-specific information"
  static const char *const GLSL_ES_STR = "OpenGL ES GLSL ES";
  if(strncmp(sl, GLSL_ES_STR, 17) == 0)
    sl += 18;

  if(sl[0] >= '0' && sl[0] <= '9' && sl[1] == '.' && sl[2] >= '0' && sl[2] <= '9')
  {
    int major = int(sl[0] - '0');
    int minor = int(sl[2] - '0');
    int ver = major * 100 + minor * 10;

    if(ver > glslVersion)
      glslVersion = ver;
  }

  if(sl[0] >= '0' && sl[0] <= '9' && sl[1] >= '0' && sl[1] <= '9' && sl[2] == '0')
  {
    int major = int(sl[0] - '0');
    int minor = int(sl[1] - '0');
    int ver = major * 100 + minor * 10;

    if(ver > glslVersion)
      glslVersion = ver;
  }
}

GLuint GLReplay::CreateMeshProgram(GLuint vs, GLuint fs, GLuint gs)
{
  GLuint program = CreateShaderProgram(vs, fs, gs);

  // set attrib locations
  GL.glBindAttribLocation(program, 0, "position");
  GL.glBindAttribLocation(program, 1, "IN_secondary");

  // relink
  GL.glLinkProgram(program);

  // check that the relink succeeded
  char buffer[1024] = {};
  GLint status = 0;
  GL.glGetProgramiv(program, eGL_LINK_STATUS, &status);
  if(status == 0)
  {
    GL.glGetProgramInfoLog(program, 1024, NULL, buffer);
    RDCERR("Link error: %s", buffer);
  }

  // detach the shaders
  GL.glDetachShader(program, vs);
  GL.glDetachShader(program, fs);
  if(gs)
    GL.glDetachShader(program, gs);

  // bind the UBO
  BindUBO(program, "MeshUBOData", 0);

  return program;
}

void GLReplay::ConfigureTexDisplayProgramBindings(GLuint program)
{
  GLint location = -1;

  GL.glUseProgram(program);

// since we split the shader up by type, not all texture slots are always available. Also on GLES
// some texture slots might be missing due to lack of extensions. So we need to check for a location
// of -1
#define SET_TEX_BINDING(name, bind)                  \
  location = GL.glGetUniformLocation(program, name); \
  if(location >= 0)                                  \
    GL.glUniform1i(location, bind);

  SET_TEX_BINDING("texUInt1D", 1);
  SET_TEX_BINDING("texUInt2D", 2);
  SET_TEX_BINDING("texUInt3D", 3);
  SET_TEX_BINDING("texUInt1DArray", 5);
  SET_TEX_BINDING("texUInt2DArray", 6);
  SET_TEX_BINDING("texUInt2DRect", 8);
  SET_TEX_BINDING("texUIntBuffer", 9);
  SET_TEX_BINDING("texUInt2DMS", 10);
  SET_TEX_BINDING("texUInt2DMSArray", 11);

  SET_TEX_BINDING("texSInt1D", 1);
  SET_TEX_BINDING("texSInt2D", 2);
  SET_TEX_BINDING("texSInt3D", 3);
  SET_TEX_BINDING("texSInt1DArray", 5);
  SET_TEX_BINDING("texSInt2DArray", 6);
  SET_TEX_BINDING("texSInt2DRect", 8);
  SET_TEX_BINDING("texSIntBuffer", 9);
  SET_TEX_BINDING("texSInt2DMS", 10);
  SET_TEX_BINDING("texSInt2DMSArray", 11);

  SET_TEX_BINDING("tex1D", 1);
  SET_TEX_BINDING("tex2D", 2);
  SET_TEX_BINDING("tex3D", 3);
  SET_TEX_BINDING("texCube", 4);
  SET_TEX_BINDING("tex1DArray", 5);
  SET_TEX_BINDING("tex2DArray", 6);
  SET_TEX_BINDING("texCubeArray", 7);
  SET_TEX_BINDING("tex2DRect", 8);
  SET_TEX_BINDING("texBuffer", 9);
  SET_TEX_BINDING("tex2DMS", 10);
  SET_TEX_BINDING("tex2DMSArray", 11);

#undef SET_TEX_BINDING
}

void GLReplay::BindUBO(GLuint program, const char *name, GLuint binding)
{
  GL.glUniformBlockBinding(program, GL.glGetUniformBlockIndex(program, name), binding);
}

void GLReplay::InitDebugData()
{
  if(m_pDriver == NULL)
    return;

  // don't reflect any shaders or programs we make
  m_pDriver->PushInternalShader();

  m_HighlightCache.driver = m_pDriver->GetReplay();

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

  {
    WindowingData window = {WindowingSystem::Headless};
    uint64_t id = MakeOutputWindow(window, true);

    m_DebugCtx = NULL;

    if(id == 0)
      return;

    m_DebugID = id;
    m_DebugCtx = &m_OutputWindows[id];

    MakeCurrentReplayContext(m_DebugCtx);

    m_pDriver->RegisterDebugCallback();
  }

  WrappedOpenGL &drv = *m_pDriver;

  DebugData.outWidth = 0.0f;
  DebugData.outHeight = 0.0f;

  rdcstr vs;
  rdcstr fs;
  rdcstr gs;
  rdcstr cs;

  ShaderType shaderType;
  int glslVersion;
  int glslBaseVer;
  int glslCSVer;    // compute shader

  GetGLSLVersions(shaderType, glslVersion, glslBaseVer, glslCSVer);

  rdcstr texSampleDefines;

  if(IsGLES)
  {
    if(HasExt[OES_texture_cube_map_array] || HasExt[EXT_texture_cube_map_array] || GLCoreVersion >= 32)
      texSampleDefines += "#define TEXSAMPLE_CUBE_ARRAY 1\n";

    if(HasExt[OES_texture_cube_map_array])
      texSampleDefines += "#extension GL_OES_texture_cube_map_array : require\n";

    if(HasExt[EXT_texture_cube_map_array])
      texSampleDefines += "#extension GL_EXT_texture_cube_map_array : require\n";

    if(HasExt[EXT_texture_buffer])
      texSampleDefines +=
          "#define TEXSAMPLE_BUFFER 1\n"
          "#extension GL_EXT_texture_buffer : require\n";

    texSampleDefines += "#define HAS_BIT_CONVERSION 1\n";
  }
  else
  {
    if(HasExt[ARB_texture_cube_map_array])
      texSampleDefines +=
          "#define TEXSAMPLE_CUBE_ARRAY 1\n"
          "#extension GL_ARB_texture_cube_map_array : require\n";

    if(HasExt[ARB_texture_multisample])
      texSampleDefines +=
          "#define TEXSAMPLE_MULTISAMPLE 1\n"
          "#extension GL_ARB_texture_multisample : require\n";

    if(HasExt[ARB_gpu_shader5])
      texSampleDefines +=
          "#define HAS_BIT_CONVERSION 1\n"
          "#extension GL_ARB_gpu_shader5 : require\n";
    else if(HasExt[ARB_shader_bit_encoding])
      texSampleDefines +=
          "#define HAS_BIT_CONVERSION 1\n"
          "#extension GL_ARB_shader_bit_encoding : require\n";
  }

  DebugData.texSampleDefines = texSampleDefines;

  rdcstr vsDefines = "#define FORCE_IO_LOCATION 1";

  if(!IsGLES)
  {
    vsDefines =
        "#extension GL_ARB_separate_shader_objects : require\n"
        "#extension GL_ARB_explicit_attrib_location : require\n" +
        vsDefines;
  }

  vs = GenerateGLSLShader(GetEmbeddedResource(glsl_blit_vert), shaderType, glslBaseVer, vsDefines);

  // used to combine with custom shaders.
  // this has to have explicit locations on the output even though we don't normally use that,
  // because GL doesn't have a fallback to match by name, and custom shaders are expected to have an
  // explicit location on the input
  DebugData.texDisplayVertexShader = CreateShader(eGL_VERTEX_SHADER, vs);

  vs = GenerateGLSLShader(GetEmbeddedResource(glsl_blit_vert), shaderType, glslBaseVer);
  fs = GenerateGLSLShader(GetEmbeddedResource(glsl_fixedcol_frag), shaderType, glslBaseVer);
  DebugData.fullScreenFixedColProg = CreateShaderProgram(vs, fs);

  fs = GenerateGLSLShader(GetEmbeddedResource(glsl_depth_copy_frag), shaderType, glslBaseVer);
  DebugData.fullScreenCopyDepth = CreateShaderProgram(vs, fs);

  fs = GenerateGLSLShader(GetEmbeddedResource(glsl_depth_copyms_frag), shaderType, glslBaseVer);
  DebugData.fullScreenCopyDepthMS = CreateShaderProgram(vs, fs);

  DebugData.fixedcolFragShaderSPIRV = DebugData.quadoverdrawFragShaderSPIRV = 0;

  // pre-compile SPIR-V shaders up front since this is more expensive
  if(HasExt[ARB_gl_spirv])
  {
    // SPIR-V shaders are always generated as desktop GL 430, for ease
    rdcstr source =
        GenerateGLSLShader(GetEmbeddedResource(glsl_fixedcol_frag), ShaderType::GLSPIRV, 430);
    DebugData.fixedcolFragShaderSPIRV = CreateSPIRVShader(eGL_FRAGMENT_SHADER, source);

    if(HasExt[ARB_gpu_shader5] && HasExt[ARB_shader_image_load_store])
    {
      rdcstr defines = "";

      if(!HasExt[ARB_derivative_control])
      {
        defines += "#define dFdxFine dFdx\n\n";
        defines += "#define dFdyFine dFdy\n\n";
      }

      source = GenerateGLSLShader(GetEmbeddedResource(glsl_quadwrite_frag), ShaderType::GLSPIRV,
                                  430, defines);
      DebugData.quadoverdrawFragShaderSPIRV = CreateSPIRVShader(eGL_FRAGMENT_SHADER, source);
    }
  }

  for(int i = 0; i < 3; i++)
  {
    rdcstr defines = rdcstr("#define SHADER_BASETYPE ") + ToStr(i) + "\n";

    fs = GenerateGLSLShader(GetEmbeddedResource(glsl_texdisplay_frag), shaderType, glslBaseVer,
                            defines + texSampleDefines);

    DebugData.texDisplayProg[i] = CreateShaderProgram(vs, fs);

    BindUBO(DebugData.texDisplayProg[i], "TexDisplayUBOData", 0);
    BindUBO(DebugData.texDisplayProg[i], "HeatmapData", 1);
    ConfigureTexDisplayProgramBindings(DebugData.texDisplayProg[i]);

    fs = GenerateGLSLShader(GetEmbeddedResource(glsl_texremap_frag), shaderType, glslBaseVer,
                            defines + texSampleDefines);

    DebugData.texRemapProg[i] = CreateShaderProgram(vs, fs);

    BindUBO(DebugData.texRemapProg[i], "TexDisplayUBOData", 0);
    ConfigureTexDisplayProgramBindings(DebugData.texRemapProg[i]);
  }

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

  if(GLCoreVersion >= 43 && !IsGLES)
  {
    GLint numsl = 0;
    drv.glGetIntegerv(eGL_NUM_SHADING_LANGUAGE_VERSIONS, &numsl);

    for(GLint i = 0; i < numsl; i++)
    {
      const char *sl = (const char *)drv.glGetStringi(eGL_SHADING_LANGUAGE_VERSION, (GLuint)i);

      CheckGLSLVersion(sl, glslVersion);
    }
  }
  else
  {
    const char *sl = (const char *)drv.glGetString(eGL_SHADING_LANGUAGE_VERSION);

    CheckGLSLVersion(sl, glslVersion);
  }

  DebugData.glslVersion = glslVersion;

  RDCLOG("GLSL version %d", glslVersion);

  vs = GenerateGLSLShader(GetEmbeddedResource(glsl_blit_vert), shaderType, glslBaseVer);

  DebugData.fixedcolFragShader = DebugData.quadoverdrawFragShader = 0;
  DebugData.quadoverdrawResolveProg = 0;

  if(IsGLES)
  {
    // quad overdraw not supported on GLES.
    // 1.
    //   dFdx doesn't support uints - potentially workaroundable with float casts, but highly
    //   doubtful GLES compilers will do that properly without exploding.
    // 2.
    //   quad overdraw write shader must be linked with user shaders in program, which requires
    //   matching ESSL version and features required for it aren't exposed as extensions to older
    //   versions but only in core versions.
  }
  else if(HasExt[ARB_shader_image_load_store] && HasExt[ARB_gpu_shader5])
  {
    fs = GenerateGLSLShader(GetEmbeddedResource(glsl_quadresolve_frag), shaderType, glslBaseVer);

    DebugData.quadoverdrawResolveProg = CreateShaderProgram(vs, fs);

    GL.glUseProgram(DebugData.quadoverdrawResolveProg);

    GL.glUniform1i(GL.glGetUniformLocation(DebugData.quadoverdrawResolveProg, "overdrawImage"), 0);
  }
  else
  {
    RDCWARN(
        "GL_ARB_shader_image_load_store/GL_ARB_gpu_shader5 not supported, disabling quad overdraw "
        "feature.");
    m_pDriver->AddDebugMessage(MessageCategory::Portability, MessageSeverity::Medium,
                               MessageSource::RuntimeWarning,
                               "GL_ARB_shader_image_load_store/GL_ARB_gpu_shader5 not supported, "
                               "disabling quad overdraw feature.");
  }

  fs = GenerateGLSLShader(GetEmbeddedResource(glsl_checkerboard_frag), shaderType, glslBaseVer);
  DebugData.checkerProg = CreateShaderProgram(vs, fs);

  BindUBO(DebugData.checkerProg, "CheckerboardUBOData", 0);

  for(size_t intIdx = 0; intIdx < 3; intIdx++)
  {
    for(size_t numViews = 0; numViews < ARRAY_COUNT(DebugData.discardProg[intIdx]); numViews++)
    {
      rdcstr defines;

      defines += rdcstr("#define SHADER_BASETYPE ") + ToStr(intIdx) + "\n";

      if(numViews > 0 && IsGLES && HasExt[OVR_multiview])
        defines = StringFormat::Fmt("#define NUM_VIEWS %zu\n", numViews + 1);

      rdcstr blitvs =
          GenerateGLSLShader(GetEmbeddedResource(glsl_blit_vert), shaderType, glslBaseVer, defines);

      fs = GenerateGLSLShader(GetEmbeddedResource(glsl_discard_frag), shaderType, glslBaseVer,
                              defines);
      DebugData.discardProg[intIdx][numViews] = CreateShaderProgram(blitvs, fs);

      BindUBO(DebugData.discardProg[intIdx][numViews], "DiscardUBOData", 0);

      if(!IsGLES)
      {
        rdcstr name = "col0";
        for(GLuint i = 0; i < 8; i++)
        {
          name[3] = char('0' + i);
          GL.glBindFragDataLocation(DebugData.discardProg[intIdx][numViews], i, name.c_str());
        }
      }
    }
  }

  {
    ResourceFormat fmt;
    fmt.type = ResourceFormatType::Regular;
    fmt.compType = CompType::Float;
    fmt.compByteWidth = 4;
    fmt.compCount = 1;
    bytebuf pattern = GetDiscardPattern(DiscardType::InvalidateCall, fmt, 1, true);
    fmt.compType = CompType::UInt;
    pattern.append(GetDiscardPattern(DiscardType::InvalidateCall, fmt, 1, true));
    drv.glGenBuffers(1, &DebugData.discardPatternBuffer);
    drv.glBindBuffer(eGL_UNIFORM_BUFFER, DebugData.discardPatternBuffer);
    drv.glNamedBufferDataEXT(DebugData.discardPatternBuffer, pattern.size(), pattern.data(),
                             eGL_STATIC_DRAW);
  }

  if(HasExt[ARB_geometry_shader4])
  {
    vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer);
    fs = GenerateGLSLShader(GetEmbeddedResource(glsl_trisize_frag), shaderType, glslBaseVer);
    gs = GenerateGLSLShader(GetEmbeddedResource(glsl_trisize_geom), shaderType, glslBaseVer);

    // create the shaders
    GLuint vsShad = CreateShader(eGL_VERTEX_SHADER, vs);
    GLuint trifsShad = CreateShader(eGL_FRAGMENT_SHADER, fs);
    GLuint gsShad = CreateShader(eGL_GEOMETRY_SHADER, gs);

    DebugData.trisizeProg = CreateMeshProgram(vsShad, trifsShad, gsShad);

    // bind trisize-unique viewport size UBO
    BindUBO(DebugData.trisizeProg, "ViewportSizeUBO", 2);

    GL.glDeleteShader(trifsShad);
    GL.glDeleteShader(gsShad);

    // we have two fragment shaders, one that reads from the vs outputs and one that reads from the
    // gs outputs
    rdcstr vsfs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_frag), shaderType, glslBaseVer,
                                     "#define SECONDARY_NAME vsout_secondary\n"
                                     "#define NORM_NAME vsout_norm\n");
    rdcstr gsfs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_frag), shaderType, glslBaseVer,
                                     "#define SECONDARY_NAME gsout_secondary\n"
                                     "#define NORM_NAME gsout_norm\n");
    gs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_geom), shaderType, glslBaseVer);

    // recreate the shaders
    GLuint vsfsShad = CreateShader(eGL_FRAGMENT_SHADER, vsfs);
    GLuint gsfsShad = CreateShader(eGL_FRAGMENT_SHADER, gsfs);
    gsShad = CreateShader(eGL_GEOMETRY_SHADER, gs);

    DebugData.meshProg[0] = CreateMeshProgram(vsShad, vsfsShad);
    DebugData.meshgsProg[0] = CreateMeshProgram(vsShad, gsfsShad, gsShad);

    if(HasExt[ARB_gpu_shader_fp64] && HasExt[ARB_vertex_attrib_64bit])
    {
      rdcstr extensions =
          "#extension GL_ARB_gpu_shader_fp64 : require\n"
          "#extension GL_ARB_vertex_attrib_64bit : require\n";

      // position only dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions + "#define POSITION_TYPE dvec4\n");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[1] = CreateMeshProgram(vsShad, vsfsShad);
      DebugData.meshgsProg[1] = CreateMeshProgram(vsShad, gsfsShad, gsShad);

      // secondary only dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions + "#define SECONDARY_TYPE dvec4\n");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[2] = CreateMeshProgram(vsShad, vsfsShad);
      DebugData.meshgsProg[2] = CreateMeshProgram(vsShad, gsfsShad, gsShad);

      // both dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions +
                                  "#define POSITION_TYPE dvec4\n"
                                  "#define SECONDARY_TYPE dvec4\n");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[3] = CreateMeshProgram(vsShad, vsfsShad);
      DebugData.meshgsProg[3] = CreateMeshProgram(vsShad, gsfsShad, gsShad);
    }
    else
    {
      // we don't warn about the lack of double support, assuming that if the driver doesn't support
      // it then it's highly unlikely that the capture uses it.
      DebugData.meshProg[1] = DebugData.meshProg[2] = DebugData.meshProg[3] = 0;
      DebugData.meshgsProg[1] = DebugData.meshgsProg[2] = DebugData.meshgsProg[3] = 0;
    }

    GL.glDeleteShader(vsShad);
    GL.glDeleteShader(vsfsShad);
    GL.glDeleteShader(gsfsShad);
    GL.glDeleteShader(gsShad);
  }
  else
  {
    vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer);

    // without a geometry shader, the fragment shader always reads from vs outputs
    fs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_frag), shaderType, glslBaseVer,
                            "#define SECONDARY_NAME vsout_secondary\n"
                            "#define NORM_NAME vsout_norm\n");

    // create the shaders
    GLuint vsShad = CreateShader(eGL_VERTEX_SHADER, vs);
    GLuint fsShad = CreateShader(eGL_FRAGMENT_SHADER, fs);

    DebugData.meshProg[0] = CreateMeshProgram(vsShad, fsShad);
    RDCEraseEl(DebugData.meshgsProg);
    DebugData.trisizeProg = 0;

    const char *warning_msg =
        "GL_ARB_geometry_shader4/GL_EXT_geometry_shader not supported, disabling triangle size and "
        "lit solid shading feature.";
    RDCWARN(warning_msg);
    m_pDriver->AddDebugMessage(MessageCategory::Portability, MessageSeverity::Medium,
                               MessageSource::RuntimeWarning, warning_msg);

    if(HasExt[ARB_gpu_shader_fp64] && HasExt[ARB_vertex_attrib_64bit])
    {
      rdcstr extensions =
          "#extension GL_ARB_gpu_shader_fp64 : require\n"
          "#extension GL_ARB_vertex_attrib_64bit : require\n";

      // position only dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions + "#define POSITION_TYPE dvec4");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[1] = CreateMeshProgram(vsShad, fsShad);

      // secondary only dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions + "#define SECONDARY_TYPE dvec4");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[2] = CreateMeshProgram(vsShad, fsShad);

      // both dvec4
      vs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_vert), shaderType, glslBaseVer,
                              extensions +
                                  "#define POSITION_TYPE dvec4\n"
                                  "#define SECONDARY_TYPE dvec4");

      // delete old shader and recreate with new source
      GL.glDeleteShader(vsShad);
      vsShad = CreateShader(eGL_VERTEX_SHADER, vs);

      DebugData.meshProg[3] = CreateMeshProgram(vsShad, fsShad);
    }
    else
    {
      // we don't warn about the lack of double support, assuming that if the driver doesn't support
      // it then it's highly unlikely that the capture uses it.
      DebugData.meshProg[1] = DebugData.meshProg[2] = DebugData.meshProg[3] = 0;
    }

    GL.glDeleteShader(vsShad);
    GL.glDeleteShader(fsShad);
  }

  cs = GenerateGLSLShader(GetEmbeddedResource(glsl_debug_math_comp), shaderType, glslCSVer);
  DebugData.shaderDebugMathProg = CreateCShaderProgram(cs);

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

  drv.glGenBuffers(ARRAY_COUNT(DebugData.UBOs), DebugData.UBOs);
  for(size_t i = 0; i < ARRAY_COUNT(DebugData.UBOs); i++)
  {
    drv.glBindBuffer(eGL_UNIFORM_BUFFER, DebugData.UBOs[i]);
    drv.glNamedBufferDataEXT(DebugData.UBOs[i], 2048, NULL, eGL_DYNAMIC_DRAW);
    RDCCOMPILE_ASSERT(sizeof(TexDisplayUBOData) <= 2048, "UBO too small");
    RDCCOMPILE_ASSERT(sizeof(FontUBOData) <= 2048, "UBO too small");
    RDCCOMPILE_ASSERT(sizeof(HistogramUBOData) <= 2048, "UBO too small");
  }

  DebugData.overlayTexWidth = DebugData.overlayTexHeight = DebugData.overlayTexSamples = 0;
  DebugData.overlayTex = DebugData.overlayFBO = 0;

  DebugData.overlayProg = 0;

  drv.glGenFramebuffers(1, &DebugData.customFBO);
  drv.glBindFramebuffer(eGL_FRAMEBUFFER, DebugData.customFBO);
  DebugData.customTex = 0;

  drv.glGenFramebuffers(1, &DebugData.pickPixelFBO);
  drv.glBindFramebuffer(eGL_FRAMEBUFFER, DebugData.pickPixelFBO);

  if(HasExt[ARB_texture_buffer_object])
  {
    drv.glGenBuffers(1, &DebugData.dummyTexBufferStore);
    drv.glBindBuffer(eGL_TEXTURE_BUFFER, DebugData.dummyTexBufferStore);
    drv.glNamedBufferDataEXT(DebugData.dummyTexBufferStore, 32, NULL, eGL_STATIC_DRAW);
    drv.glBindBuffer(eGL_TEXTURE_BUFFER, 0);

    drv.glGenTextures(1, &DebugData.dummyTexBuffer);
    drv.glBindTexture(eGL_TEXTURE_BUFFER, DebugData.dummyTexBuffer);
    drv.glTextureBufferEXT(DebugData.dummyTexBuffer, eGL_TEXTURE_BUFFER, eGL_RGBA32F,
                           DebugData.dummyTexBufferStore);
    drv.glBindTexture(eGL_TEXTURE_BUFFER, 0);
  }
  else
  {
    DebugData.dummyTexBuffer = DebugData.dummyTexBufferStore = 0;
  }

  drv.glGenTextures(1, &DebugData.pickPixelTex);
  drv.glBindTexture(eGL_TEXTURE_2D, DebugData.pickPixelTex);

  drv.glTextureImage2DEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, 0, eGL_RGBA32F, 1, 1, 0, eGL_RGBA,
                          eGL_FLOAT, NULL);
  drv.glTextureParameteriEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, eGL_TEXTURE_MAX_LEVEL, 0);
  drv.glTextureParameteriEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, eGL_TEXTURE_MIN_FILTER,
                             eGL_NEAREST);
  drv.glTextureParameteriEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, eGL_TEXTURE_MAG_FILTER,
                             eGL_NEAREST);
  drv.glTextureParameteriEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, eGL_TEXTURE_WRAP_S,
                             eGL_CLAMP_TO_EDGE);
  drv.glTextureParameteriEXT(DebugData.pickPixelTex, eGL_TEXTURE_2D, eGL_TEXTURE_WRAP_T,
                             eGL_CLAMP_TO_EDGE);
  drv.glFramebufferTexture2D(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0, eGL_TEXTURE_2D,
                             DebugData.pickPixelTex, 0);

  drv.glGenVertexArrays(1, &DebugData.emptyVAO);
  drv.glBindVertexArray(DebugData.emptyVAO);

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

  // histogram/minmax data
  {
    RDCEraseEl(DebugData.minmaxTileProgram);
    RDCEraseEl(DebugData.histogramProgram);
    RDCEraseEl(DebugData.minmaxResultProgram);

    RDCCOMPILE_ASSERT(
        ARRAY_COUNT(DebugData.minmaxTileProgram) >= (TEXDISPLAY_SINT_TEX | TEXDISPLAY_TYPEMASK) + 1,
        "not enough programs");

    if(HasExt[ARB_compute_shader] && HasExt[ARB_shader_storage_buffer_object] &&
       HasExt[ARB_shading_language_420pack])
    {
      for(int t = 1; t <= RESTYPE_TEXTYPEMAX; t++)
      {
        // float, uint, sint
        for(int i = 0; i < 3; i++)
        {
          int idx = t;
          if(i == 1)
            idx |= TEXDISPLAY_UINT_TEX;
          if(i == 2)
            idx |= TEXDISPLAY_SINT_TEX;

          {
            rdcstr defines;
            defines += rdcstr("#define SHADER_RESTYPE ") + ToStr(t) + "\n";
            defines += rdcstr("#define SHADER_BASETYPE ") + ToStr(i) + "\n";
            defines += texSampleDefines;

            cs = GenerateGLSLShader(GetEmbeddedResource(glsl_minmaxtile_comp), shaderType,
                                    glslCSVer, defines);

            DebugData.minmaxTileProgram[idx] = CreateCShaderProgram(cs);

            BindUBO(DebugData.minmaxTileProgram[idx], "HistogramUBOData", 2);
            ConfigureTexDisplayProgramBindings(DebugData.minmaxTileProgram[idx]);
          }

          {
            rdcstr defines;
            defines += rdcstr("#define SHADER_RESTYPE ") + ToStr(t) + "\n";
            defines += rdcstr("#define SHADER_BASETYPE ") + ToStr(i) + "\n";
            defines += texSampleDefines;

            cs = GenerateGLSLShader(GetEmbeddedResource(glsl_histogram_comp), shaderType, glslCSVer,
                                    defines);

            DebugData.histogramProgram[idx] = CreateCShaderProgram(cs);

            BindUBO(DebugData.histogramProgram[idx], "HistogramUBOData", 2);
            ConfigureTexDisplayProgramBindings(DebugData.histogramProgram[idx]);
          }

          if(t == 1)
          {
            rdcstr defines;
            defines += rdcstr("#define SHADER_RESTYPE ") + ToStr(t) + "\n";
            defines += rdcstr("#define SHADER_BASETYPE ") + ToStr(i) + "\n";

            cs = GenerateGLSLShader(GetEmbeddedResource(glsl_minmaxresult_comp), shaderType,
                                    glslCSVer, defines);

            DebugData.minmaxResultProgram[i] = CreateCShaderProgram(cs);

            BindUBO(DebugData.minmaxResultProgram[i], "HistogramUBOData", 2);
            ConfigureTexDisplayProgramBindings(DebugData.minmaxResultProgram[i]);
          }
        }
      }
    }
    else
    {
      RDCWARN(
          "GL_ARB_compute_shader or ARB_shader_storage_buffer_object not supported, disabling "
          "min/max and histogram features.");
      m_pDriver->AddDebugMessage(MessageCategory::Portability, MessageSeverity::Medium,
                                 MessageSource::RuntimeWarning,
                                 "GL_ARB_compute_shader or ARB_shader_storage_buffer_object not "
                                 "supported, disabling min/max and histogram features.");
    }

    drv.glGenBuffers(1, &DebugData.minmaxTileResult);
    drv.glGenBuffers(1, &DebugData.minmaxResult);
    drv.glGenBuffers(1, &DebugData.histogramBuf);

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

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

    drv.glNamedBufferDataEXT(DebugData.minmaxTileResult, byteSize, NULL, eGL_DYNAMIC_DRAW);
    drv.glNamedBufferDataEXT(DebugData.minmaxResult, sizeof(Vec4f) * 2, NULL, eGL_DYNAMIC_READ);
    drv.glNamedBufferDataEXT(DebugData.histogramBuf, sizeof(uint32_t) * HGRAM_NUM_BUCKETS, NULL,
                             eGL_DYNAMIC_READ);
  }

  if(HasExt[ARB_compute_shader] && HasExt[ARB_shading_language_420pack])
  {
    cs = GenerateGLSLShader(GetEmbeddedResource(glsl_mesh_comp), shaderType, glslCSVer);
    DebugData.meshPickProgram = CreateCShaderProgram(cs);

    BindUBO(DebugData.meshPickProgram, "MeshPickUBOData", 0);
  }
  else
  {
    DebugData.meshPickProgram = 0;
    RDCWARN("GL_ARB_compute_shader not supported, disabling mesh picking.");
    m_pDriver->AddDebugMessage(MessageCategory::Portability, MessageSeverity::Medium,
                               MessageSource::RuntimeWarning,
                               "GL_ARB_compute_shader not supported, disabling mesh picking.");
  }

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

  DebugData.pickResultBuf = 0;

  if(DebugData.meshPickProgram)
  {
    drv.glGenBuffers(1, &DebugData.pickResultBuf);
    drv.glBindBuffer(eGL_SHADER_STORAGE_BUFFER, DebugData.pickResultBuf);
    drv.glNamedBufferDataEXT(DebugData.pickResultBuf,
                             sizeof(Vec4f) * DebugRenderData::maxMeshPicks + sizeof(uint32_t) * 4,
                             NULL, eGL_DYNAMIC_READ);

    // sized/created on demand
    DebugData.pickVBBuf = DebugData.pickIBBuf = 0;
    DebugData.pickVBSize = DebugData.pickIBSize = 0;
  }

  drv.glGenVertexArrays(1, &DebugData.meshVAO);
  drv.glBindVertexArray(DebugData.meshVAO);

  drv.glGenBuffers(1, &DebugData.axisFrustumBuffer);
  drv.glBindBuffer(eGL_ARRAY_BUFFER, DebugData.axisFrustumBuffer);

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

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

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

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

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

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

  drv.glNamedBufferDataEXT(DebugData.axisFrustumBuffer, sizeof(axisFrustum), axisFrustum,
                           eGL_STATIC_DRAW);

  drv.glGenVertexArrays(1, &DebugData.axisVAO);
  drv.glBindVertexArray(DebugData.axisVAO);
  drv.glVertexAttribPointer(0, 3, eGL_FLOAT, GL_FALSE, sizeof(Vec3f), NULL);
  drv.glEnableVertexAttribArray(0);

  drv.glGenVertexArrays(1, &DebugData.frustumVAO);
  drv.glBindVertexArray(DebugData.frustumVAO);
  drv.glVertexAttribPointer(0, 3, eGL_FLOAT, GL_FALSE, sizeof(Vec3f),
                            (const void *)(sizeof(Vec3f) * 6));
  drv.glEnableVertexAttribArray(0);

  drv.glGenVertexArrays(1, &DebugData.triHighlightVAO);
  drv.glBindVertexArray(DebugData.triHighlightVAO);

  drv.glGenBuffers(1, &DebugData.triHighlightBuffer);
  drv.glBindBuffer(eGL_ARRAY_BUFFER, DebugData.triHighlightBuffer);

  drv.glNamedBufferDataEXT(DebugData.triHighlightBuffer, sizeof(Vec4f) * 24, NULL, eGL_DYNAMIC_DRAW);

  drv.glVertexAttribPointer(0, 4, eGL_FLOAT, GL_FALSE, sizeof(Vec4f), NULL);
  drv.glEnableVertexAttribArray(0);

  MakeCurrentReplayContext(&m_ReplayCtx);

  // try to identify the GPU we're running on.
  {
    RDCEraseEl(m_DriverInfo);

    const char *vendor = (const char *)drv.glGetString(eGL_VENDOR);
    const char *renderer = (const char *)drv.glGetString(eGL_RENDERER);
    const char *version = (const char *)drv.glGetString(eGL_VERSION);

    // we're just doing substring searches, so combine both for ease.
    rdcstr combined = (vendor ? vendor : "");
    combined += " ";
    combined += (renderer ? renderer : "");

    // make lowercase, for case-insensitive matching, and add preceding/trailing space for easier
    // 'word' matching
    combined = " " + strlower(combined) + " ";

    RDCDEBUG("Identifying vendor from '%s'", combined.c_str());

    struct pattern
    {
      const char *search;
      GPUVendor vendor;
    } patterns[] = {
        {" arm ", GPUVendor::ARM},
        {" mali ", GPUVendor::ARM},
        {" mali-", GPUVendor::ARM},
        {" amd ", GPUVendor::AMD},
        {"advanced micro devices", GPUVendor::AMD},
        {"ati technologies", GPUVendor::AMD},
        {"radeon", GPUVendor::AMD},
        {"broadcom", GPUVendor::Broadcom},
        {"imagination", GPUVendor::Imagination},
        {"powervr", GPUVendor::Imagination},
        {"intel", GPUVendor::Intel},
        {"geforce", GPUVendor::nVidia},
        {"quadro", GPUVendor::nVidia},
        {"nouveau", GPUVendor::nVidia},
        {"nvidia", GPUVendor::nVidia},
        {"adreno", GPUVendor::Qualcomm},
        {"qualcomm", GPUVendor::Qualcomm},
        {"vivante", GPUVendor::Verisilicon},
        {"llvmpipe", GPUVendor::Software},
        {"softpipe", GPUVendor::Software},
        {"bluestacks", GPUVendor::Software},
    };

    for(const pattern &p : patterns)
    {
      if(combined.contains(p.search))
      {
        if(m_DriverInfo.vendor == GPUVendor::Unknown)
        {
          m_DriverInfo.vendor = p.vendor;
        }
        else
        {
          // either we already found this with another pattern, or we've identified two patterns and
          // it's ambiguous. Keep the first one we found, arbitrarily, but print a warning.
          if(m_DriverInfo.vendor != p.vendor)
          {
            RDCWARN("Already identified '%s' as %s, but now identified as %s", combined.c_str(),
                    ToStr(m_DriverInfo.vendor).c_str(), ToStr(p.vendor).c_str());
          }
        }
      }
    }

    RDCDEBUG("Identified GPU vendor '%s'", ToStr(m_DriverInfo.vendor).c_str());

    rdcstr versionString = version;

    versionString += " / ";
    versionString += renderer ? renderer : "";
    versionString += " / ";
    versionString += vendor ? vendor : "";

    versionString.resize(RDCMIN(versionString.size(), ARRAY_COUNT(m_DriverInfo.version) - 1));
    memcpy(m_DriverInfo.version, versionString.c_str(), versionString.size());
  }

  // these below need to be made on the replay context, as they are context-specific (not shared)
  // and will be used on the replay context.

  if(HasExt[ARB_transform_feedback2])
    drv.glGenTransformFeedbacks(1, &DebugData.feedbackObj);
  drv.glGenBuffers(1, &DebugData.feedbackBuffer);
  DebugData.feedbackQueries.push_back(0);
  drv.glGenQueries(1, &DebugData.feedbackQueries[0]);

  if(HasExt[ARB_transform_feedback2])
    drv.glBindTransformFeedback(eGL_TRANSFORM_FEEDBACK, DebugData.feedbackObj);
  drv.glBindBuffer(eGL_TRANSFORM_FEEDBACK_BUFFER, DebugData.feedbackBuffer);
  drv.glNamedBufferDataEXT(DebugData.feedbackBuffer, (GLsizeiptr)DebugData.feedbackBufferSize, NULL,
                           eGL_DYNAMIC_READ);
  drv.glBindBufferBase(eGL_TRANSFORM_FEEDBACK_BUFFER, 0, DebugData.feedbackBuffer);
  if(HasExt[ARB_transform_feedback2])
    drv.glBindTransformFeedback(eGL_TRANSFORM_FEEDBACK, 0);

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

  if(!HasExt[ARB_gpu_shader5])
  {
    RDCWARN(
        "ARB_gpu_shader5 not supported, pixel picking and saving of integer textures may be "
        "inaccurate.");
    m_pDriver->AddDebugMessage(MessageCategory::Portability, MessageSeverity::Medium,
                               MessageSource::RuntimeWarning,
                               "ARB_gpu_shader5 not supported, pixel picking and saving of integer "
                               "textures may be inaccurate.");

    m_Degraded = true;
  }

  if(!HasExt[ARB_stencil_texturing])
  {
    RDCWARN("ARB_stencil_texturing not supported, stencil values will not be displayed or picked.");
    m_pDriver->AddDebugMessage(
        MessageCategory::Portability, MessageSeverity::Medium, MessageSource::RuntimeWarning,
        "ARB_stencil_texturing not supported, stencil values will not be displayed or picked.");

    m_Degraded = true;
  }

  if(!HasExt[ARB_shader_image_load_store] || !HasExt[ARB_compute_shader] ||
     !HasExt[ARB_shading_language_420pack])
  {
    RDCWARN(
        "Don't have shader image load/store or compute shaders, functionality will be degraded.");
    m_Degraded = true;
  }

#if ENABLED(RDOC_APPLE)
  // temporary hack - just never consider apple degraded, since there's never going to be an
  // improvement so there's no point warning users.
  m_Degraded = false;
#endif

  m_pDriver->PopInternalShader();
}

void GLReplay::DeleteDebugData()
{
  WrappedOpenGL &drv = *m_pDriver;

  MakeCurrentReplayContext(&m_ReplayCtx);

  if(HasExt[KHR_debug])
  {
    GL.glDebugMessageCallback(NULL, NULL);
  }

  if(DebugData.overlayProg)
    drv.glDeleteProgram(DebugData.overlayProg);

  for(size_t b = 0; b < 3; b++)
    for(size_t i = 0; i < ARRAY_COUNT(DebugData.discardProg[b]); i++)
      if(DebugData.discardProg[b][i])
        drv.glDeleteProgram(DebugData.discardProg[b][i]);

  if(HasExt[ARB_transform_feedback2])
    drv.glDeleteTransformFeedbacks(1, &DebugData.feedbackObj);
  drv.glDeleteBuffers(1, &DebugData.feedbackBuffer);
  drv.glDeleteQueries((GLsizei)DebugData.feedbackQueries.size(), DebugData.feedbackQueries.data());

  MakeCurrentReplayContext(m_DebugCtx);

  if(HasExt[KHR_debug])
  {
    GL.glDebugMessageCallback(NULL, NULL);
  }

  ClearPostVSCache();

  drv.glDeleteFramebuffers(1, &DebugData.overlayFBO);
  drv.glDeleteTextures(1, &DebugData.overlayTex);

  if(DebugData.fullScreenFixedColProg)
    drv.glDeleteProgram(DebugData.fullScreenFixedColProg);
  if(DebugData.fullScreenCopyDepth)
    drv.glDeleteProgram(DebugData.fullScreenCopyDepth);
  if(DebugData.fullScreenCopyDepthMS)
    drv.glDeleteProgram(DebugData.fullScreenCopyDepthMS);

  if(DebugData.quadoverdrawFragShader)
    drv.glDeleteShader(DebugData.quadoverdrawFragShader);
  if(DebugData.quadoverdrawFragShaderSPIRV)
    drv.glDeleteShader(DebugData.quadoverdrawFragShaderSPIRV);
  if(DebugData.quadoverdrawResolveProg)
    drv.glDeleteProgram(DebugData.quadoverdrawResolveProg);

  if(DebugData.texDisplayVertexShader)
    drv.glDeleteShader(DebugData.texDisplayVertexShader);
  for(int i = 0; i < 3; i++)
  {
    if(DebugData.texDisplayProg[i])
      drv.glDeleteProgram(DebugData.texDisplayProg[i]);
    if(DebugData.texRemapProg[i])
      drv.glDeleteProgram(DebugData.texRemapProg[i]);
  }

  if(DebugData.checkerProg)
    drv.glDeleteProgram(DebugData.checkerProg);
  if(DebugData.fixedcolFragShader)
    drv.glDeleteShader(DebugData.fixedcolFragShader);
  if(DebugData.fixedcolFragShaderSPIRV)
    drv.glDeleteShader(DebugData.fixedcolFragShaderSPIRV);

  for(size_t i = 0; i < ARRAY_COUNT(DebugData.meshProg); i++)
  {
    if(DebugData.meshProg[i])
      drv.glDeleteProgram(DebugData.meshProg[i]);
    if(DebugData.meshgsProg[i])
      drv.glDeleteProgram(DebugData.meshgsProg[i]);
  }
  if(DebugData.trisizeProg)
    drv.glDeleteProgram(DebugData.trisizeProg);

  if(DebugData.shaderDebugMathProg)
    drv.glDeleteProgram(DebugData.shaderDebugMathProg);

  for(auto it = m_ShaderDebugSampleProg.begin(); it != m_ShaderDebugSampleProg.end(); ++it)
  {
    drv.glDeleteProgram(it->second);
  }

  drv.glDeleteBuffers(ARRAY_COUNT(DebugData.UBOs), DebugData.UBOs);
  drv.glDeleteFramebuffers(1, &DebugData.pickPixelFBO);
  drv.glDeleteTextures(1, &DebugData.pickPixelTex);

  drv.glDeleteTextures(1, &DebugData.dummyTexBuffer);
  drv.glDeleteBuffers(1, &DebugData.dummyTexBufferStore);

  drv.glDeleteFramebuffers(1, &DebugData.customFBO);
  drv.glDeleteTextures(1, &DebugData.customTex);

  drv.glDeleteVertexArrays(1, &DebugData.emptyVAO);

  for(int t = 1; t <= RESTYPE_TEXTYPEMAX; t++)
  {
    // float, uint, sint
    for(int i = 0; i < 3; i++)
    {
      int idx = t;
      if(i == 1)
        idx |= TEXDISPLAY_UINT_TEX;
      if(i == 2)
        idx |= TEXDISPLAY_SINT_TEX;

      if(DebugData.minmaxTileProgram[idx])
        drv.glDeleteProgram(DebugData.minmaxTileProgram[idx]);
      if(DebugData.histogramProgram[idx])
        drv.glDeleteProgram(DebugData.histogramProgram[idx]);

      if(t == 1)
      {
        if(DebugData.minmaxResultProgram[i])
          drv.glDeleteProgram(DebugData.minmaxResultProgram[i]);
      }
    }
  }

  if(DebugData.meshPickProgram)
    drv.glDeleteProgram(DebugData.meshPickProgram);
  drv.glDeleteBuffers(1, &DebugData.pickIBBuf);
  drv.glDeleteBuffers(1, &DebugData.pickVBBuf);
  drv.glDeleteBuffers(1, &DebugData.pickResultBuf);

  drv.glDeleteBuffers(1, &DebugData.minmaxTileResult);
  drv.glDeleteBuffers(1, &DebugData.minmaxResult);
  drv.glDeleteBuffers(1, &DebugData.histogramBuf);

  drv.glDeleteVertexArrays(1, &DebugData.meshVAO);
  drv.glDeleteVertexArrays(1, &DebugData.axisVAO);
  drv.glDeleteVertexArrays(1, &DebugData.frustumVAO);
  drv.glDeleteVertexArrays(1, &DebugData.triHighlightVAO);

  drv.glDeleteBuffers(1, &DebugData.axisFrustumBuffer);
  drv.glDeleteBuffers(1, &DebugData.triHighlightBuffer);
}

GLuint GLReplay::GetShaderDebugMathProg()
{
  return DebugData.shaderDebugMathProg;
}

GLuint GLReplay::MakeShaderDebugSampleProg(const SamplingProgramConfig &config)
{
  uint32_t hash = config.hashKey();

  if(m_ShaderDebugSampleProg[hash])
    return m_ShaderDebugSampleProg[hash];

  rdcstr defines;

  defines += rdcstr("#define SHADER_BASETYPE ") + ToStr((uint32_t)config.resType) + "\n";

  defines += StringFormat::Fmt("#define FETCH_OFFSET ivec3(%u, %u, %u)\n", config.fetchOffset.x,
                               config.fetchOffset.y, config.fetchOffset.z);

  defines += StringFormat::Fmt(
      "#define GATHER_OFFSETS ivec2[4](ivec2(%u, %u), ivec2(%u, %u), ivec2(%u, %u), ivec2(%u, "
      "%u))\n",
      config.gatherOffsets[0], config.gatherOffsets[1], config.gatherOffsets[2],
      config.gatherOffsets[3], config.gatherOffsets[4], config.gatherOffsets[5],
      config.gatherOffsets[6], config.gatherOffsets[7]);

  defines += StringFormat::Fmt("#define USE_GRAD %u\n", config.useGrad);
  defines += StringFormat::Fmt("#define USE_GATHER_OFFS %u\n", config.useGatherOffs);
  defines += StringFormat::Fmt("#define GATHER_CHANNEL %u\n", config.gatherChannel);

  rdcstr operation = "Do";
  switch(config.op)
  {
    case SamplingProgramConfig::Fetch: operation += "Fetch"; break;
    case SamplingProgramConfig::QueryLod: operation += "QueryLod"; break;
    case SamplingProgramConfig::Sample: operation += "Sample"; break;
    case SamplingProgramConfig::SampleDref: operation += "SampleDref"; break;
    case SamplingProgramConfig::Gather: operation += "Gather"; break;
    case SamplingProgramConfig::GatherDref: operation += "GatherDref"; break;
  }

  rdcstr dim;
  switch(config.dim)
  {
    case SamplingProgramConfig::TexBuffer: dim = "Buffer"; break;
    case SamplingProgramConfig::Tex1D: dim = "1D"; break;
    case SamplingProgramConfig::Tex2D: dim = "2D"; break;
    case SamplingProgramConfig::Tex3D: dim = "3D"; break;
    case SamplingProgramConfig::TexCube: dim = "Cube"; break;
    case SamplingProgramConfig::Tex1DArray: dim = "1DArray"; break;
    case SamplingProgramConfig::Tex2DArray: dim = "2DArray"; break;
    case SamplingProgramConfig::Tex3DArray: dim = "3DArray"; break;
    case SamplingProgramConfig::TexCubeArray: dim = "CubeArray"; break;
    case SamplingProgramConfig::Tex2DRect: dim = "2DRect"; break;
    case SamplingProgramConfig::Tex2DMS: dim = "2DMS"; break;
    case SamplingProgramConfig::Tex2DMSArray: dim = "2DMSArray"; break;
  }

  rdcstr manualBias = config.manualBias ? "Bias" : "";

  if(config.manualBias)
  {
    if(config.op == SamplingProgramConfig::Sample)
    {
      switch(config.dim)
      {
        case SamplingProgramConfig::Tex2D:
        case SamplingProgramConfig::Tex3D:
        case SamplingProgramConfig::TexCube:
        case SamplingProgramConfig::Tex2DArray:
        case SamplingProgramConfig::TexCubeArray:
          // these are fine
          break;
        default: RDCERR("Unsupported dimension %u with manual bias on sample", config.dim);
      }
    }
    else if(config.op == SamplingProgramConfig::SampleDref)
    {
      RDCASSERT(config.dim == SamplingProgramConfig::Tex2D, (uint32_t)config.dim);
    }
    else
    {
      RDCERR("Unsupported operation with manual bias");
    }
  }

  defines += StringFormat::Fmt("#define OPERATION %s%s%s\n", operation.c_str(), manualBias.c_str(),
                               dim.c_str());

  defines += StringFormat::Fmt("#define USE_GRAD %u\n", config.useGrad);

  ShaderType shaderType;
  int glslVersion;
  int glslBaseVer;
  int glslCSVer;    // compute shader

  GetGLSLVersions(shaderType, glslVersion, glslBaseVer, glslCSVer);

  if(!IsGLES)
  {
    glslVersion = 330;
    if(GLCoreVersion >= 45)
    {
      glslVersion = 450;
      defines += "#extension GL_ARB_sparse_texture_clamp : require\n";
      defines += "#define ENABLE_MINLOD 1\n";
    }

    if(HasExt[ARB_gpu_shader5] && HasExt[ARB_texture_gather])
    {
      defines += "#extension GL_ARB_texture_gather : require\n";
      defines += "#extension GL_ARB_gpu_shader5 : require\n";
      defines += "#define GATHER_SUPPORT 2\n";
    }
    else if(HasExt[ARB_texture_gather])
    {
      defines += "#extension GL_ARB_texture_gather : require\n";
      defines += "#define GATHER_SUPPORT 1\n";
    }
    else
    {
      defines += "#define GATHER_SUPPORT 0\n";
    }
  }
  else
  {
    defines += "#define ENABLE_MINLOD 0\n";
    if(HasExt[ARB_gpu_shader5] && HasExt[ARB_texture_gather])
    {
      defines += "#define GATHER_SUPPORT 2\n";
    }
    else if(HasExt[ARB_texture_gather])
    {
      defines += "#define GATHER_SUPPORT 1\n";
    }
    else
    {
      defines += "#define GATHER_SUPPORT 0\n";
    }
  }

  rdcstr vs =
      GenerateGLSLShader(GetEmbeddedResource(glsl_shaderdebug_sample_vert), shaderType, glslCSVer);
  rdcstr ps = GenerateGLSLShader(GetEmbeddedResource(glsl_debug_sample_frag), shaderType, glslCSVer,
                                 defines + DebugData.texSampleDefines);
  GLuint prog = CreateShaderProgram(vs, ps, "");

  if(config.op == SamplingProgramConfig::SampleDref || config.op == SamplingProgramConfig::GatherDref)
    dim += "Shadow";

  dim = "tex" + dim;

  BindUBO(prog, "debugsample", 0);
  GL.glUniform1i(GL.glGetUniformLocation(prog, dim.c_str()), 0);

  return m_ShaderDebugSampleProg[hash] = prog;
}

GLReplay::TextureSamplerState GLReplay::SetSamplerParams(GLenum target, GLuint texname,
                                                         TextureSamplerMode mode)
{
  TextureSamplerState ret;

  if(target == eGL_TEXTURE_BUFFER || target == eGL_TEXTURE_2D_MULTISAMPLE ||
     target == eGL_TEXTURE_2D_MULTISAMPLE_ARRAY)
    return ret;

  // fetch previous texture sampler settings
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_MIN_FILTER, (GLint *)&ret.minFilter);
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_MAG_FILTER, (GLint *)&ret.magFilter);
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_S, (GLint *)&ret.wrapS);
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_T, (GLint *)&ret.wrapT);
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_R, (GLint *)&ret.wrapR);
  GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_COMPARE_MODE, (GLint *)&ret.compareMode);
  if(!IsGLES)
    GL.glGetTextureParameterfvEXT(texname, target, eGL_TEXTURE_LOD_BIAS, &ret.lodBias);

  // disable depth comparison
  GLenum compareMode = eGL_NONE;
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_COMPARE_MODE, (GLint *)&compareMode);

  // disable LOD bias, we control our own sampling
  float lodBias = 0.0f;
  if(!IsGLES)
    GL.glTextureParameterfvEXT(texname, target, eGL_TEXTURE_LOD_BIAS, &lodBias);

  // always want to clamp
  GLenum param = eGL_CLAMP_TO_EDGE;
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_S, (GLint *)&param);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_T, (GLint *)&param);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_R, (GLint *)&param);

  // depending on the mode, set min/mag filter
  GLenum minFilter = eGL_NEAREST;
  GLenum magFilter = eGL_NEAREST;

  switch(mode)
  {
    case TextureSamplerMode::Point:
      minFilter = eGL_NEAREST_MIPMAP_NEAREST;
      magFilter = eGL_NEAREST;
      break;
    case TextureSamplerMode::PointNoMip:
      minFilter = eGL_NEAREST;
      magFilter = eGL_NEAREST;
      break;
    case TextureSamplerMode::Linear:
      minFilter = eGL_LINEAR;
      magFilter = eGL_LINEAR;
      break;
  }

  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MIN_FILTER, (GLint *)&minFilter);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAG_FILTER, (GLint *)&magFilter);

  return ret;
}

void GLReplay::RestoreSamplerParams(GLenum target, GLuint texname, TextureSamplerState state)
{
  if(target == eGL_TEXTURE_BUFFER || target == eGL_TEXTURE_2D_MULTISAMPLE ||
     target == eGL_TEXTURE_2D_MULTISAMPLE_ARRAY)
    return;

  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_S, (GLint *)&state.wrapS);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_T, (GLint *)&state.wrapT);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_WRAP_R, (GLint *)&state.wrapR);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MIN_FILTER, (GLint *)&state.minFilter);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAG_FILTER, (GLint *)&state.magFilter);
  GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_COMPARE_MODE, (GLint *)&state.compareMode);
  if(!IsGLES)
    GL.glTextureParameterfvEXT(texname, target, eGL_TEXTURE_LOD_BIAS, &state.lodBias);
}

void GLReplay::FillWithDiscardPattern(DiscardType type, GLuint framebuffer, GLsizei numAttachments,
                                      const GLenum *attachments, GLint x, GLint y, GLsizei width,
                                      GLsizei height)
{
  RDCASSERT(type == DiscardType::InvalidateCall);

  WrappedOpenGL &drv = *m_pDriver;

  GLMarkerRegion region("FillWithDiscardPattern FBO");

  GLRenderState rs;
  rs.FetchState(&drv);

  // we use the original framebuffer, and mask off any attachments that shouldn't be discarded
  drv.glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, framebuffer);

  int numviews = 1;

  if(IsGLES && HasExt[OVR_multiview])
  {
    GL.glGetNamedFramebufferAttachmentParameterivEXT(
        framebuffer, eGL_COLOR_ATTACHMENT0, eGL_FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR,
        &numviews);
  }

  drv.glBindBufferBase(eGL_UNIFORM_BUFFER, 0, DebugData.discardPatternBuffer);

  drv.glDisable(eGL_BLEND);
  drv.glDisable(eGL_CULL_FACE);
  drv.glEnable(eGL_DEPTH_TEST);
  drv.glEnable(eGL_STENCIL_TEST);
  drv.glDepthFunc(eGL_ALWAYS);
  drv.glStencilFunc(eGL_ALWAYS, 0, 0xff);
  if(!IsGLES)
    drv.glPolygonMode(eGL_FRONT_AND_BACK, eGL_FILL);

  // scissor to the rect
  drv.glEnable(eGL_SCISSOR_TEST);
  drv.glScissor(x, y, width, height);

  GLint maxview[2] = {2048, 2048};
  drv.glGetIntegerv(eGL_MAX_VIEWPORT_DIMS, maxview);
  drv.glViewport(0, 0, maxview[0], maxview[1]);

  // disable all masks at first
  drv.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
  drv.glDepthMask(GL_FALSE);
  drv.glStencilMask(0);

  int intIdx = -1;

  // enable masks specified by attachments
  bool stencil = false;
  for(GLsizei i = 0; i < numAttachments; i++)
  {
    if(attachments[i] == eGL_DEPTH_STENCIL_ATTACHMENT)
    {
      drv.glDepthMask(GL_TRUE);
      drv.glStencilMask(0xff);
      stencil = true;
    }
    else if(attachments[i] == eGL_DEPTH_ATTACHMENT)
    {
      drv.glDepthMask(GL_TRUE);
    }
    else if(attachments[i] == eGL_STENCIL_ATTACHMENT)
    {
      drv.glStencilMask(0xff);
      stencil = true;
    }
    else
    {
      drv.glColorMaski(attachments[i] - eGL_COLOR_ATTACHMENT0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

      if(intIdx == -1)
      {
        GLuint att;
        drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, attachments[i],
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                  (GLint *)&att);
        GLenum attType;
        drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, attachments[i],
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
                                                  (GLint *)&attType);

        ResourceId id = drv.GetResourceManager()->GetResID(attType == eGL_RENDERBUFFER
                                                               ? RenderbufferRes(drv.GetCtx(), att)
                                                               : TextureRes(drv.GetCtx(), att));

        GLenum fmt = m_pDriver->m_Textures[id].internalFormat;

        if(IsUIntFormat(fmt))
          intIdx = 1;
        else if(IsSIntFormat(fmt))
          intIdx = 2;
        else
          intIdx = 0;
      }
    }
  }

  if(intIdx == -1)
    intIdx = 0;

  GLuint prog = DebugData.discardProg[intIdx][RDCCLAMP(numviews, 1, 4) - 1];

  drv.glUseProgram(prog);

  GLint loc = drv.glGetUniformLocation(prog, "flags");

  uint32_t flags = 0U;

  if(height == 1)
    flags |= 0x10U;

  if(stencil)
  {
    drv.glStencilOp(eGL_REPLACE, eGL_REPLACE, eGL_REPLACE);

    drv.glStencilFunc(eGL_ALWAYS, 0, 0xff);
    drv.glProgramUniform1ui(prog, loc, flags | 1U);
    drv.glDrawArrays(eGL_TRIANGLE_STRIP, 0, 4);

    drv.glStencilFunc(eGL_ALWAYS, 0xff, 0xff);
    drv.glProgramUniform1ui(prog, loc, flags | 2U);
    drv.glDrawArrays(eGL_TRIANGLE_STRIP, 0, 4);
  }
  else
  {
    drv.glProgramUniform1ui(prog, loc, flags);

    // if we're not writing stencil we can just do one draw
    drv.glDrawArrays(eGL_TRIANGLE_STRIP, 0, 4);
  }

  rs.ApplyState(&drv);
}

void GLReplay::FillWithDiscardPattern(DiscardType type, ResourceId id, GLuint mip, GLint xoffset,
                                      GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
                                      GLsizei depth)
{
  RDCASSERT(type == DiscardType::InvalidateCall);

  WrappedOpenGL &drv = *m_pDriver;

  GLMarkerRegion region("FillWithDiscardPattern Texture");

  WrappedOpenGL::TextureData &texDetails = drv.m_Textures[id];

  GLenum fmt = texDetails.internalFormat;

  bytebuf &pattern = m_DiscardPatterns[fmt];

  GLenum target = texDetails.curType;

  if(pattern.empty())
    pattern = GetDiscardPattern(type, MakeResourceFormat(target, fmt), 1, true);

  bool compressed = IsCompressedFormat(fmt);

  if(target == eGL_TEXTURE_CUBE_MAP)
    depth = RDCMIN(depth, 6);
  else
    depth = RDCMIN(texDetails.depth, depth);
  height = RDCMIN(texDetails.height, height);
  width = RDCMIN(texDetails.width, width);

  GLint dim = texDetails.dimension;

  GLint oldRowLength = 0;
  GL.glGetIntegerv(eGL_UNPACK_ROW_LENGTH, &oldRowLength);

  // ensure even if we're uploading a sub-rect that the row length is the same.
  GL.glPixelStorei(eGL_UNPACK_ROW_LENGTH, DiscardPatternWidth);

  GLuint tex = texDetails.resource.name;

  GLenum format = eGL_NONE;
  GLenum datatype = eGL_NONE;

  if(!compressed)
  {
    format = GetBaseFormat(fmt);
    datatype = GetDataType(fmt);
  }

  for(GLsizei zc = 0; zc < depth; zc++)
  {
    GLsizei z = zoffset + zc;

    if(texDetails.curType == eGL_TEXTURE_CUBE_MAP)
    {
      GLenum targets[] = {
          eGL_TEXTURE_CUBE_MAP_POSITIVE_X, eGL_TEXTURE_CUBE_MAP_NEGATIVE_X,
          eGL_TEXTURE_CUBE_MAP_POSITIVE_Y, eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
          eGL_TEXTURE_CUBE_MAP_POSITIVE_Z, eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
      };

      target = targets[z % 6];
    }

    for(GLsizei yc = 0; yc < height; yc += DiscardPatternHeight)
    {
      GLsizei y = yoffset + yc;

      for(GLsizei xc = 0; xc < width; xc += DiscardPatternWidth)
      {
        GLsizei x = xoffset + xc;

        GLsizei subwidth = RDCMIN((GLsizei)DiscardPatternWidth, texDetails.width - x);
        GLsizei subheight = RDCMIN((GLsizei)DiscardPatternHeight, texDetails.height - y);

        if(compressed)
        {
          GLsizei dataSize = (GLsizei)GetCompressedByteSize(subwidth, subheight, 1, fmt);

          if(dim == 1)
            GL.glCompressedTextureSubImage1DEXT(tex, target, mip, x, subwidth, fmt, dataSize,
                                                pattern.data());
          else if(dim == 2)
            GL.glCompressedTextureSubImage2DEXT(tex, target, mip, x, y, subwidth, subheight, fmt,
                                                dataSize, pattern.data());
          else if(dim == 3)
            GL.glCompressedTextureSubImage3DEXT(tex, target, mip, x, y, z, subwidth, subheight, 1,
                                                fmt, dataSize, pattern.data());
        }
        else
        {
          if(dim == 1)
            GL.glTextureSubImage1DEXT(tex, target, mip, x, subwidth, format, datatype,
                                      pattern.data());
          else if(dim == 2)
            GL.glTextureSubImage2DEXT(tex, target, mip, x, y, subwidth, subheight, format, datatype,
                                      pattern.data());
          else if(dim == 3)
            GL.glTextureSubImage3DEXT(tex, target, mip, x, y, z, subwidth, subheight, 1, format,
                                      datatype, pattern.data());
        }
      }
    }
  }

  GL.glPixelStorei(eGL_UNPACK_ROW_LENGTH, oldRowLength);
}

void GLReplay::PickPixel(ResourceId texture, uint32_t x, uint32_t y, const Subresource &sub,
                         CompType typeCast, float pixel[4])
{
  WrappedOpenGL &drv = *m_pDriver;

  MakeCurrentReplayContext(m_DebugCtx);

  drv.glBindFramebuffer(eGL_FRAMEBUFFER, DebugData.pickPixelFBO);
  drv.glBindFramebuffer(eGL_READ_FRAMEBUFFER, DebugData.pickPixelFBO);

  pixel[0] = pixel[1] = pixel[2] = pixel[3] = 0.0f;
  drv.glClearBufferfv(eGL_COLOR, 0, pixel);

  DebugData.outWidth = DebugData.outHeight = 1.0f;
  drv.glViewport(0, 0, 1, 1);

  TextureDisplay texDisplay;

  texDisplay.red = texDisplay.green = texDisplay.blue = texDisplay.alpha = true;
  texDisplay.flipY = false;
  texDisplay.hdrMultiplier = -1.0f;
  texDisplay.linearDisplayAsGamma = true;
  texDisplay.subresource = sub;
  texDisplay.customShaderId = ResourceId();
  texDisplay.rangeMin = 0.0f;
  texDisplay.rangeMax = 1.0f;
  texDisplay.scale = 1.0f;
  texDisplay.resourceId = texture;
  texDisplay.typeCast = typeCast;
  texDisplay.rawOutput = true;
  texDisplay.xOffset = -float(x << sub.mip);
  texDisplay.yOffset = -float(y << sub.mip);

  WrappedOpenGL::TextureData &texDetails = m_pDriver->m_Textures[texDisplay.resourceId];

  uint32_t mipWidth = RDCMAX(1U, (uint32_t)texDetails.width >> sub.mip);
  uint32_t mipHeight = RDCMAX(1U, (uint32_t)texDetails.height >> sub.mip);

  texDisplay.xOffset = -(float(x) / float(mipWidth)) * texDetails.width;
  texDisplay.yOffset = -(float(y) / float(mipHeight)) * texDetails.height;

  RenderTextureInternal(texDisplay, eTexDisplay_MipShift);

  drv.glReadPixels(0, 0, 1, 1, eGL_RGBA, eGL_FLOAT, (void *)pixel);

  if(!HasExt[ARB_gpu_shader5])
  {
    if(IsSIntFormat(texDetails.internalFormat))
    {
      int32_t casted[4] = {
          (int32_t)pixel[0],
          (int32_t)pixel[1],
          (int32_t)pixel[2],
          (int32_t)pixel[3],
      };

      memcpy(pixel, casted, sizeof(casted));
    }
    else if(IsUIntFormat(texDetails.internalFormat))
    {
      uint32_t casted[4] = {
          (uint32_t)pixel[0],
          (uint32_t)pixel[1],
          (uint32_t)pixel[2],
          (uint32_t)pixel[3],
      };

      memcpy(pixel, casted, sizeof(casted));
    }
  }

  {
    // need to read stencil separately as GL can't read both depth and stencil
    // at the same time.
    if(texDetails.internalFormat == eGL_DEPTH24_STENCIL8 ||
       texDetails.internalFormat == eGL_DEPTH32F_STENCIL8 ||
       texDetails.internalFormat == eGL_DEPTH_STENCIL)
    {
      texDisplay.red = texDisplay.blue = texDisplay.alpha = false;

      RenderTextureInternal(texDisplay, eTexDisplay_MipShift);

      uint32_t stencilpixel[4];
      drv.glReadPixels(0, 0, 1, 1, eGL_RGBA, eGL_FLOAT, (void *)stencilpixel);

      if(!HasExt[ARB_gpu_shader5])
      {
        // bits weren't aliased, so re-cast back to uint.
        float fpix[4];
        memcpy(fpix, stencilpixel, sizeof(fpix));

        stencilpixel[0] = (uint32_t)fpix[0];
        stencilpixel[1] = (uint32_t)fpix[1];
      }

      // not sure whether [0] or [1] will return stencil values, so use
      // max of two because other channel should be 0
      pixel[1] = float(RDCMAX(stencilpixel[0], stencilpixel[1])) / 255.0f;
    }
  }
}

bool GLReplay::GetMinMax(ResourceId texid, const Subresource &sub, CompType typeCast, float *minval,
                         float *maxval)
{
  WrappedOpenGL::TextureData &texDetails = m_pDriver->m_Textures[texid];

  if(!IsCompressedFormat(texDetails.internalFormat) &&
     GetBaseFormat(texDetails.internalFormat) == eGL_DEPTH_STENCIL)
  {
    // for depth/stencil we need to run the code twice - once to fetch depth and once to fetch
    // stencil - since we can't process float depth and int stencil at the same time
    Vec4f depth[2] = {
        {0.0f, 0.0f, 0.0f, 0.0f},
        {1.0f, 1.0f, 1.0f, 1.0f},
    };
    Vec4u stencil[2] = {{0, 0, 0, 0}, {1, 1, 1, 1}};

    bool success = GetMinMax(texid, sub, typeCast, false, &depth[0].x, &depth[1].x);

    if(!success)
      return false;

    success = GetMinMax(texid, sub, typeCast, true, (float *)&stencil[0].x, (float *)&stencil[1].x);

    if(!success)
      return false;

    // copy across into green channel, casting up to float, dividing by the range for this texture
    float rangeScale = 1.0f;
    switch(texDetails.internalFormat)
    {
      case eGL_STENCIL_INDEX1: rangeScale = 1.0f; break;
      case eGL_STENCIL_INDEX4: rangeScale = 16.0f; break;
      default: RDCWARN("Unexpected raw format for stencil visualization"); DELIBERATE_FALLTHROUGH();
      case eGL_DEPTH24_STENCIL8:
      case eGL_DEPTH32F_STENCIL8:
      case eGL_DEPTH_STENCIL:
      case eGL_STENCIL_INDEX8: rangeScale = 255.0f; break;
      case eGL_STENCIL_INDEX16: rangeScale = 65535.0f; break;
    }

    depth[0].y = float(stencil[0].x) / rangeScale;
    depth[1].y = float(stencil[1].x) / rangeScale;

    memcpy(minval, &depth[0], sizeof(depth[0]));
    memcpy(maxval, &depth[1], sizeof(depth[1]));

    return true;
  }

  return GetMinMax(texid, sub, typeCast, false, minval, maxval);
}

bool GLReplay::GetMinMax(ResourceId texid, const Subresource &sub, CompType typeCast, bool stencil,
                         float *minval, float *maxval)
{
  if(texid == ResourceId() || m_pDriver->m_Textures.find(texid) == m_pDriver->m_Textures.end())
    return false;

  if(!HasExt[ARB_compute_shader] || !HasExt[ARB_shading_language_420pack])
    return false;

  WrappedOpenGL::TextureData &texDetails = m_pDriver->m_Textures[texid];

  TextureDescription details = GetTexture(texid);

  int texSlot = 0;
  int intIdx = 0;

  bool renderbuffer = false;

  switch(texDetails.curType)
  {
    case eGL_RENDERBUFFER:
      texSlot = texDetails.samples > 1 ? RESTYPE_TEX2DMS : RESTYPE_TEX2D;
      renderbuffer = true;
      break;
    case eGL_TEXTURE_1D: texSlot = RESTYPE_TEX1D; break;
    default: RDCWARN("Unexpected texture type"); DELIBERATE_FALLTHROUGH();
    case eGL_TEXTURE_2D: texSlot = RESTYPE_TEX2D; break;
    case eGL_TEXTURE_2D_MULTISAMPLE: texSlot = RESTYPE_TEX2DMS; break;
    case eGL_TEXTURE_2D_MULTISAMPLE_ARRAY: texSlot = RESTYPE_TEX2DMSARRAY; break;
    case eGL_TEXTURE_RECTANGLE: texSlot = RESTYPE_TEXRECT; break;
    case eGL_TEXTURE_BUFFER: texSlot = RESTYPE_TEXBUFFER; break;
    case eGL_TEXTURE_3D: texSlot = RESTYPE_TEX3D; break;
    case eGL_TEXTURE_CUBE_MAP: texSlot = RESTYPE_TEXCUBE; break;
    case eGL_TEXTURE_1D_ARRAY: texSlot = RESTYPE_TEX1DARRAY; break;
    case eGL_TEXTURE_2D_ARRAY: texSlot = RESTYPE_TEX2DARRAY; break;
    case eGL_TEXTURE_CUBE_MAP_ARRAY: texSlot = RESTYPE_TEXCUBEARRAY; break;
  }

  GLenum target = texDetails.curType;
  GLuint texname = texDetails.resource.name;

  // do blit from renderbuffer to texture, then sample from texture
  if(renderbuffer)
  {
    // need replay context active to do blit (as FBOs aren't shared)
    MakeCurrentReplayContext(&m_ReplayCtx);

    GLMarkerRegion blitRegion("Renderbuffer Blit");

    GLuint curDrawFBO = 0;
    GLuint curReadFBO = 0;
    GL.glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, (GLint *)&curDrawFBO);
    GL.glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, (GLint *)&curReadFBO);

    GL.glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, texDetails.renderbufferFBOs[1]);
    GL.glBindFramebuffer(eGL_READ_FRAMEBUFFER, texDetails.renderbufferFBOs[0]);

    SafeBlitFramebuffer(
        0, 0, texDetails.width, texDetails.height, 0, 0, texDetails.width, texDetails.height,
        GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, eGL_NEAREST);

    GL.glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, curDrawFBO);
    GL.glBindFramebuffer(eGL_READ_FRAMEBUFFER, curReadFBO);

    texname = texDetails.renderbufferReadTex;
    target = texDetails.samples > 1 ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
  }

  MakeCurrentReplayContext(m_DebugCtx);

  GLMarkerRegion region("GetMinMax");

  RDCGLenum dsTexMode = eGL_NONE;
  if(IsDepthStencilFormat(texDetails.internalFormat))
  {
    if(stencil)
    {
      dsTexMode = eGL_STENCIL_INDEX;
      intIdx = 1;
    }
    else
    {
      dsTexMode = eGL_DEPTH_COMPONENT;
    }
  }
  else
  {
    if(details.format.compType == CompType::UInt)
      intIdx = 1;
    if(details.format.compType == CompType::SInt)
      intIdx = 2;
  }

  GL.glBindBufferBase(eGL_UNIFORM_BUFFER, 2, DebugData.UBOs[0]);
  HistogramUBOData *cdata =
      (HistogramUBOData *)GL.glMapBufferRange(eGL_UNIFORM_BUFFER, 0, sizeof(HistogramUBOData),
                                              GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);

  cdata->HistogramTextureResolution.x = (float)RDCMAX(details.width >> sub.mip, 1U);
  cdata->HistogramTextureResolution.y = (float)RDCMAX(details.height >> sub.mip, 1U);
  cdata->HistogramTextureResolution.z = (float)RDCMAX(details.depth >> sub.mip, 1U);
  if(texDetails.curType == eGL_TEXTURE_3D)
    cdata->HistogramSlice =
        (float)RDCCLAMP(sub.slice, 0U, uint32_t(details.depth >> sub.mip) - 1) + 0.001f;
  else
    cdata->HistogramSlice = (float)RDCCLAMP(sub.slice, 0U, details.arraysize - 1) + 0.001f;
  cdata->HistogramMip = (int)sub.mip;
  cdata->HistogramNumSamples = texDetails.samples;
  cdata->HistogramSample = (int)RDCCLAMP(sub.sample, 0U, details.msSamp - 1);
  if(sub.sample == ~0U)
    cdata->HistogramSample = -int(details.msSamp);
  cdata->HistogramMin = 0.0f;
  cdata->HistogramMax = 1.0f;
  cdata->HistogramChannels = 0xf;

  cdata->HistogramYUVDownsampleRate = {};
  cdata->HistogramYUVAChannels = {};

  int progIdx = texSlot;

  if(intIdx == 1)
    progIdx |= TEXDISPLAY_UINT_TEX;
  if(intIdx == 2)
    progIdx |= TEXDISPLAY_SINT_TEX;

  int blocksX = (int)ceil(cdata->HistogramTextureResolution.x /
                          float(HGRAM_PIXELS_PER_TILE * HGRAM_TILES_PER_BLOCK));
  int blocksY = (int)ceil(cdata->HistogramTextureResolution.y /
                          float(HGRAM_PIXELS_PER_TILE * HGRAM_TILES_PER_BLOCK));

  GL.glUnmapBuffer(eGL_UNIFORM_BUFFER);

  GL.glActiveTexture((RDCGLenum)(eGL_TEXTURE0 + texSlot));
  GL.glBindTexture(target, texname);

  TextureSamplerMode mode = TextureSamplerMode::Point;

  if(texSlot == RESTYPE_TEXRECT || texSlot == RESTYPE_TEXBUFFER)
    mode = TextureSamplerMode::PointNoMip;

  TextureSamplerState prevSampState = SetSamplerParams(target, texname, mode);

  GLint origDSTexMode = eGL_DEPTH_COMPONENT;
  if(dsTexMode != eGL_NONE && HasExt[ARB_stencil_texturing])
  {
    GL.glGetTextureParameterivEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, &origDSTexMode);
    GL.glTextureParameteriEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, dsTexMode);
  }

  GLint baseLevel[4] = {-1};
  GLint maxlevel[4] = {-1};
  GLint forcedparam[4] = {};

  bool levelsTex = (target != eGL_TEXTURE_BUFFER && target != eGL_TEXTURE_2D_MULTISAMPLE &&
                    target != eGL_TEXTURE_2D_MULTISAMPLE_ARRAY);

  if(levelsTex)
  {
    GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, baseLevel);
    GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, maxlevel);
  }
  else
  {
    baseLevel[0] = maxlevel[0] = -1;
  }

  // ensure texture is mipmap complete and we can view all mips (if the range has been reduced) by
  // forcing TEXTURE_MAX_LEVEL to cover all valid mips.
  if(levelsTex && texid != DebugData.CustomShaderTexID)
  {
    forcedparam[0] = 0;
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, forcedparam);
    forcedparam[0] = GLint(details.mips - 1);
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, forcedparam);
  }
  else
  {
    maxlevel[0] = -1;
  }

  GL.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 0, DebugData.minmaxTileResult);

  GL.glUseProgram(DebugData.minmaxTileProgram[progIdx]);
  GL.glDispatchCompute(blocksX, blocksY, 1);

  GL.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

  GL.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 0, DebugData.minmaxResult);
  GL.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 1, DebugData.minmaxTileResult);

  GL.glUseProgram(DebugData.minmaxResultProgram[intIdx]);
  GL.glDispatchCompute(1, 1, 1);

  GL.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

  Vec4f minmax[2];
  GL.glBindBuffer(eGL_COPY_READ_BUFFER, DebugData.minmaxResult);
  GL.glGetBufferSubData(eGL_COPY_READ_BUFFER, 0, sizeof(minmax), minmax);

  if(baseLevel[0] >= 0)
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, baseLevel);

  if(maxlevel[0] >= 0)
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, maxlevel);

  RestoreSamplerParams(target, texname, prevSampState);

  minval[0] = minmax[0].x;
  minval[1] = minmax[0].y;
  minval[2] = minmax[0].z;
  minval[3] = minmax[0].w;

  maxval[0] = minmax[1].x;
  maxval[1] = minmax[1].y;
  maxval[2] = minmax[1].z;
  maxval[3] = minmax[1].w;

  if(dsTexMode != eGL_NONE && HasExt[ARB_stencil_texturing])
    GL.glTextureParameteriEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, origDSTexMode);

  return true;
}

bool GLReplay::GetHistogram(ResourceId texid, const Subresource &sub, CompType typeCast,
                            float minval, float maxval, const rdcfixedarray<bool, 4> &channels_,
                            rdcarray<uint32_t> &histogram)
{
  if(minval >= maxval || texid == ResourceId())
    return false;

  if(m_pDriver->m_Textures.find(texid) == m_pDriver->m_Textures.end())
    return false;

  if(!HasExt[ARB_compute_shader] || !HasExt[ARB_shading_language_420pack])
    return false;

  // take a local copy so we can modify it
  rdcfixedarray<bool, 4> channels = channels_;

  WrappedOpenGL::TextureData &texDetails = m_pDriver->m_Textures[texid];

  TextureDescription details = GetTexture(texid);

  int texSlot = 0;
  int intIdx = 0;

  bool renderbuffer = false;

  switch(texDetails.curType)
  {
    case eGL_RENDERBUFFER:
      texSlot = texDetails.samples > 1 ? RESTYPE_TEX2DMS : RESTYPE_TEX2D;
      renderbuffer = true;
      break;
    case eGL_TEXTURE_1D: texSlot = RESTYPE_TEX1D; break;
    default: RDCWARN("Unexpected texture type"); DELIBERATE_FALLTHROUGH();
    case eGL_TEXTURE_2D: texSlot = RESTYPE_TEX2D; break;
    case eGL_TEXTURE_2D_MULTISAMPLE: texSlot = RESTYPE_TEX2DMS; break;
    case eGL_TEXTURE_2D_MULTISAMPLE_ARRAY: texSlot = RESTYPE_TEX2DMSARRAY; break;
    case eGL_TEXTURE_RECTANGLE: texSlot = RESTYPE_TEXRECT; break;
    case eGL_TEXTURE_BUFFER: texSlot = RESTYPE_TEXBUFFER; break;
    case eGL_TEXTURE_3D: texSlot = RESTYPE_TEX3D; break;
    case eGL_TEXTURE_CUBE_MAP: texSlot = RESTYPE_TEXCUBE; break;
    case eGL_TEXTURE_1D_ARRAY: texSlot = RESTYPE_TEX1DARRAY; break;
    case eGL_TEXTURE_2D_ARRAY: texSlot = RESTYPE_TEX2DARRAY; break;
    case eGL_TEXTURE_CUBE_MAP_ARRAY: texSlot = RESTYPE_TEXCUBEARRAY; break;
  }

  GLenum target = texDetails.curType;
  GLuint texname = texDetails.resource.name;

  // do blit from renderbuffer to texture, then sample from texture
  if(renderbuffer)
  {
    // need replay context active to do blit (as FBOs aren't shared)
    MakeCurrentReplayContext(&m_ReplayCtx);

    GLMarkerRegion blitRegion("Renderbuffer Blit");

    GLuint curDrawFBO = 0;
    GLuint curReadFBO = 0;
    GL.glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, (GLint *)&curDrawFBO);
    GL.glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, (GLint *)&curReadFBO);

    GL.glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, texDetails.renderbufferFBOs[1]);
    GL.glBindFramebuffer(eGL_READ_FRAMEBUFFER, texDetails.renderbufferFBOs[0]);

    SafeBlitFramebuffer(
        0, 0, texDetails.width, texDetails.height, 0, 0, texDetails.width, texDetails.height,
        GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, eGL_NEAREST);

    GL.glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, curDrawFBO);
    GL.glBindFramebuffer(eGL_READ_FRAMEBUFFER, curReadFBO);

    texname = texDetails.renderbufferReadTex;
    target = texDetails.samples > 1 ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
  }

  MakeCurrentReplayContext(m_DebugCtx);

  GLMarkerRegion region("GetHistogram");

  RDCGLenum dsTexMode = eGL_NONE;
  if(IsDepthStencilFormat(texDetails.internalFormat))
  {
    // stencil-only, make sure we display it as such
    if(texDetails.internalFormat == eGL_STENCIL_INDEX8)
    {
      channels[0] = false;
      channels[1] = true;
      channels[2] = false;
      channels[3] = false;
    }

    // depth-only, make sure we display it as such
    if(GetBaseFormat(texDetails.internalFormat) == eGL_DEPTH_COMPONENT)
    {
      channels[0] = true;
      channels[1] = false;
      channels[2] = false;
      channels[3] = false;
    }

    if(!channels[0] && channels[1])
    {
      dsTexMode = eGL_STENCIL_INDEX;

      // Stencil texture sampling is not normalized in OpenGL
      intIdx = 1;
      float rangeScale = 1.0f;
      switch(texDetails.internalFormat)
      {
        case eGL_STENCIL_INDEX1: rangeScale = 1.0f; break;
        case eGL_STENCIL_INDEX4: rangeScale = 16.0f; break;
        default:
          RDCWARN("Unexpected raw format for stencil visualization");
          DELIBERATE_FALLTHROUGH();
        case eGL_DEPTH24_STENCIL8:
        case eGL_DEPTH32F_STENCIL8:
        case eGL_DEPTH_STENCIL:
        case eGL_STENCIL_INDEX8: rangeScale = 255.0f; break;
        case eGL_STENCIL_INDEX16: rangeScale = 65535.0f; break;
      }
      minval *= rangeScale;
      maxval *= rangeScale;
    }
    else
    {
      dsTexMode = eGL_DEPTH_COMPONENT;
    }
  }
  else
  {
    if(details.format.compType == CompType::UInt)
      intIdx = 1;
    if(details.format.compType == CompType::SInt)
      intIdx = 2;
  }

  GL.glBindBufferBase(eGL_UNIFORM_BUFFER, 2, DebugData.UBOs[0]);
  HistogramUBOData *cdata =
      (HistogramUBOData *)GL.glMapBufferRange(eGL_UNIFORM_BUFFER, 0, sizeof(HistogramUBOData),
                                              GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);

  cdata->HistogramTextureResolution.x = (float)RDCMAX(details.width >> sub.mip, 1U);
  cdata->HistogramTextureResolution.y = (float)RDCMAX(details.height >> sub.mip, 1U);
  cdata->HistogramTextureResolution.z = (float)RDCMAX(details.depth >> sub.mip, 1U);
  if(texDetails.curType == eGL_TEXTURE_3D)
    cdata->HistogramSlice =
        (float)RDCCLAMP(sub.slice, 0U, uint32_t(details.depth >> sub.mip) - 1) + 0.001f;
  else
    cdata->HistogramSlice = (float)RDCCLAMP(sub.slice, 0U, details.arraysize - 1) + 0.001f;
  cdata->HistogramMip = sub.mip;
  cdata->HistogramNumSamples = texDetails.samples;
  cdata->HistogramSample = (int)RDCCLAMP(sub.sample, 0U, details.msSamp - 1);
  if(sub.sample == ~0U)
    cdata->HistogramSample = -int(details.msSamp);
  cdata->HistogramMin = minval;

  cdata->HistogramYUVDownsampleRate = {};
  cdata->HistogramYUVAChannels = {};

  // The calculation in the shader normalises each value between min and max, then multiplies by the
  // number of buckets.
  // But any value equal to HistogramMax must go into NUM_BUCKETS-1, so add a small delta.
  cdata->HistogramMax = maxval + maxval * 1e-6f;

  cdata->HistogramChannels = 0;
  if(dsTexMode == eGL_NONE)
  {
    if(channels[0])
      cdata->HistogramChannels |= 0x1;
    if(channels[1])
      cdata->HistogramChannels |= 0x2;
    if(channels[2])
      cdata->HistogramChannels |= 0x4;
    if(channels[3])
      cdata->HistogramChannels |= 0x8;
  }
  else
  {
    // Both depth and stencil texture mode use the red channel
    cdata->HistogramChannels |= 0x1;
  }
  cdata->HistogramFlags = 0;

  int progIdx = texSlot;

  if(intIdx == 1)
    progIdx |= TEXDISPLAY_UINT_TEX;
  if(intIdx == 2)
    progIdx |= TEXDISPLAY_SINT_TEX;

  int blocksX = (int)ceil(cdata->HistogramTextureResolution.x /
                          float(HGRAM_PIXELS_PER_TILE * HGRAM_TILES_PER_BLOCK));
  int blocksY = (int)ceil(cdata->HistogramTextureResolution.y /
                          float(HGRAM_PIXELS_PER_TILE * HGRAM_TILES_PER_BLOCK));

  GL.glUnmapBuffer(eGL_UNIFORM_BUFFER);

  GL.glActiveTexture((RDCGLenum)(eGL_TEXTURE0 + texSlot));
  GL.glBindTexture(target, texname);

  TextureSamplerMode mode = TextureSamplerMode::Point;

  if(texSlot == RESTYPE_TEXRECT || texSlot == RESTYPE_TEXBUFFER)
    mode = TextureSamplerMode::PointNoMip;

  TextureSamplerState prevSampState = SetSamplerParams(target, texname, mode);

  GLint origDSTexMode = eGL_DEPTH_COMPONENT;
  if(dsTexMode != eGL_NONE && HasExt[ARB_stencil_texturing])
  {
    GL.glGetTextureParameterivEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, &origDSTexMode);
    GL.glTextureParameteriEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, dsTexMode);
  }

  GLint baseLevel[4] = {-1};
  GLint maxlevel[4] = {-1};
  GLint forcedparam[4] = {};

  bool levelsTex = (target != eGL_TEXTURE_BUFFER && target != eGL_TEXTURE_2D_MULTISAMPLE &&
                    target != eGL_TEXTURE_2D_MULTISAMPLE_ARRAY);

  if(levelsTex)
  {
    GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, baseLevel);
    GL.glGetTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, maxlevel);
  }
  else
  {
    baseLevel[0] = maxlevel[0] = -1;
  }

  // ensure texture is mipmap complete and we can view all mips (if the range has been reduced) by
  // forcing TEXTURE_MAX_LEVEL to cover all valid mips.
  if(levelsTex && texid != DebugData.CustomShaderTexID)
  {
    forcedparam[0] = 0;
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, forcedparam);
    forcedparam[0] = GLint(details.mips - 1);
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, forcedparam);
  }
  else
  {
    maxlevel[0] = -1;
  }

  GL.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 0, DebugData.histogramBuf);

  GLuint zero = 0;
  GL.glClearBufferData(eGL_SHADER_STORAGE_BUFFER, eGL_R32UI, eGL_RED_INTEGER, eGL_UNSIGNED_INT,
                       &zero);

  GL.glUseProgram(DebugData.histogramProgram[progIdx]);
  GL.glDispatchCompute(blocksX, blocksY, 1);

  GL.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);

  histogram.clear();
  histogram.resize(HGRAM_NUM_BUCKETS);

  GL.glBindBuffer(eGL_COPY_READ_BUFFER, DebugData.histogramBuf);
  GL.glGetBufferSubData(eGL_COPY_READ_BUFFER, 0, sizeof(uint32_t) * HGRAM_NUM_BUCKETS, &histogram[0]);

  if(baseLevel[0] >= 0)
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_BASE_LEVEL, baseLevel);

  if(maxlevel[0] >= 0)
    GL.glTextureParameterivEXT(texname, target, eGL_TEXTURE_MAX_LEVEL, maxlevel);

  RestoreSamplerParams(target, texname, prevSampState);

  if(dsTexMode != eGL_NONE && HasExt[ARB_stencil_texturing])
    GL.glTextureParameteriEXT(texname, target, eGL_DEPTH_STENCIL_TEXTURE_MODE, origDSTexMode);

  return true;
}

uint32_t GLReplay::PickVertex(uint32_t eventId, int32_t width, int32_t height,
                              const MeshDisplay &cfg, uint32_t x, uint32_t y)
{
  WrappedOpenGL &drv = *m_pDriver;

  if(!HasExt[ARB_compute_shader] || !HasExt[ARB_shading_language_420pack])
    return ~0U;

  MakeCurrentReplayContext(m_DebugCtx);

  drv.glUseProgram(DebugData.meshPickProgram);

  float nearPlane = cfg.cam ? ((Camera *)cfg.cam)->GetNear() : 0.1f;
  float farPlane = cfg.cam ? ((Camera *)cfg.cam)->GetFar() : 100000.0f;

  Matrix4f projMat = Matrix4f::Perspective(90.0f, nearPlane, farPlane, float(width) / float(height));

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

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

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

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

    guessProjInverse = guessProj.Inverse();
  }

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

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

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

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

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

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

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

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

      Matrix4f pickMVPguessProjInverse = guessProj.Mul(inversePickMVP);

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

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

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

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

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

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

      farPosCamera = nearPosCamera + rayDir;

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

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

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

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

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

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

  GLuint ib = 0;

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

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

  if(cfg.position.indexByteStride && cfg.position.indexResourceId != ResourceId())
    ib = m_pDriver->GetResourceManager()->GetResource(cfg.position.indexResourceId).name;

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

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

  if(ib)
  {
    rdcarray<uint32_t> idxtmp;

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

      numIndices *= 3;
    }

    // resize up on demand
    if(DebugData.pickIBBuf == 0 || DebugData.pickIBSize < numIndices * sizeof(uint32_t))
    {
      drv.glDeleteBuffers(1, &DebugData.pickIBBuf);

      drv.glGenBuffers(1, &DebugData.pickIBBuf);
      drv.glBindBuffer(eGL_SHADER_STORAGE_BUFFER, DebugData.pickIBBuf);
      drv.glNamedBufferDataEXT(DebugData.pickIBBuf, numIndices * sizeof(uint32_t), NULL,
                               eGL_STREAM_DRAW);

      DebugData.pickIBSize = numIndices * sizeof(uint32_t);
    }

    byte *idxs = new byte[numIndices * cfg.position.indexByteStride];
    memset(idxs, 0, numIndices * cfg.position.indexByteStride);

    rdcarray<uint32_t> outidxs;
    outidxs.resize(numIndices);

    drv.glBindBuffer(eGL_COPY_READ_BUFFER, ib);

    GLuint bufsize = 0;
    drv.glGetBufferParameteriv(eGL_COPY_READ_BUFFER, eGL_BUFFER_SIZE, (GLint *)&bufsize);

    drv.glGetBufferSubData(eGL_COPY_READ_BUFFER, (GLintptr)cfg.position.indexByteOffset,
                           RDCMIN(uint32_t(bufsize) - uint32_t(cfg.position.indexByteOffset),
                                  cfg.position.numIndices * cfg.position.indexByteStride),
                           idxs);

    uint8_t *idxs8 = (uint8_t *)idxs;
    uint16_t *idxs16 = (uint16_t *)idxs;
    uint32_t *idxs32 = (uint32_t *)idxs;

    size_t idxcount = 0;

    if(cfg.position.indexByteStride == 1)
    {
      for(uint32_t i = 0; i < cfg.position.numIndices; i++)
      {
        uint32_t idx = idxs8[i];

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

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

        outidxs[i] = idx;
        idxcount++;
      }
    }
    else if(cfg.position.indexByteStride == 2)
    {
      for(uint32_t i = 0; i < cfg.position.numIndices; i++)
      {
        uint32_t idx = idxs16[i];

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

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

        outidxs[i] = idx;
        idxcount++;
      }
    }
    else
    {
      for(uint32_t i = 0; i < cfg.position.numIndices; i++)
      {
        uint32_t idx = idxs32[i];

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

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

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

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

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

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

      numIndices = (uint32_t)outidxs.size();
    }

    drv.glBindBuffer(eGL_SHADER_STORAGE_BUFFER, DebugData.pickIBBuf);
    drv.glBufferSubData(eGL_SHADER_STORAGE_BUFFER, 0, numIndices * sizeof(uint32_t), outidxs.data());

    delete[] idxs;
  }

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

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

    if(DebugData.pickVBBuf == 0 || DebugData.pickVBSize < (maxIndex + 1) * sizeof(Vec4f))
    {
      drv.glDeleteBuffers(1, &DebugData.pickVBBuf);

      drv.glGenBuffers(1, &DebugData.pickVBBuf);
      drv.glBindBuffer(eGL_SHADER_STORAGE_BUFFER, DebugData.pickVBBuf);
      drv.glNamedBufferDataEXT(DebugData.pickVBBuf, (maxIndex + 1) * sizeof(Vec4f), NULL,
                               eGL_DYNAMIC_DRAW);

      DebugData.pickVBSize = (maxIndex + 1) * sizeof(Vec4f);
    }

    rdcarray<FloatVector> vbData;
    vbData.resize(maxIndex + 1);

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

    bool valid;

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

    drv.glBindBuffer(eGL_SHADER_STORAGE_BUFFER, DebugData.pickVBBuf);
    drv.glBufferSubData(eGL_SHADER_STORAGE_BUFFER, 0, (maxIndex + 1) * sizeof(Vec4f), vbData.data());
  }

  drv.glBindBufferBase(eGL_UNIFORM_BUFFER, 0, DebugData.UBOs[0]);
  MeshPickUBOData *cdata =
      (MeshPickUBOData *)drv.glMapBufferRange(eGL_UNIFORM_BUFFER, 0, sizeof(MeshPickUBOData),
                                              GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);

  if(!cdata)
  {
    RDCERR("Map buffer failed %d", drv.glGetError());
    return ~0U;
  }

  cdata->rayPos = rayPos;
  cdata->rayDir = rayDir;
  cdata->use_indices = cfg.position.indexByteStride ? 1U : 0U;
  cdata->numVerts = numIndices;
  bool isTriangleMesh = true;
  switch(cfg.position.topology)
  {
    case Topology::TriangleList:
    {
      cdata->meshMode = MESH_TRIANGLE_LIST;
      break;
    }
    case Topology::TriangleStrip:
    {
      cdata->meshMode = MESH_TRIANGLE_STRIP;
      break;
    }
    case Topology::TriangleFan:
    {
      if(fandecode)
        cdata->meshMode = MESH_TRIANGLE_LIST;
      else
        cdata->meshMode = MESH_TRIANGLE_FAN;
      break;
    }
    case Topology::TriangleList_Adj:
    {
      cdata->meshMode = MESH_TRIANGLE_LIST_ADJ;
      break;
    }
    case Topology::TriangleStrip_Adj:
    {
      cdata->meshMode = MESH_TRIANGLE_STRIP_ADJ;
      break;
    }
    default:    // points, lines, patchlists, unknown
    {
      cdata->meshMode = MESH_OTHER;
      isTriangleMesh = false;
    }
  }

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

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

  drv.glUnmapBuffer(eGL_UNIFORM_BUFFER);

  uint32_t reset[4] = {};
  drv.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 0, DebugData.pickResultBuf);
  drv.glBufferSubData(eGL_SHADER_STORAGE_BUFFER, 0, sizeof(uint32_t) * 4, &reset);

  drv.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 1, DebugData.pickVBBuf);
  drv.glBindBufferRange(eGL_SHADER_STORAGE_BUFFER, 2, DebugData.pickIBBuf, (GLintptr)0,
                        (GLsizeiptr)(sizeof(uint32_t) * cfg.position.numIndices));
  drv.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 3, DebugData.pickResultBuf);

  drv.glDispatchCompute(GLuint((cfg.position.numIndices) / 128 + 1), 1, 1);
  drv.glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);

  uint32_t numResults = 0;

  drv.glBindBuffer(eGL_COPY_READ_BUFFER, DebugData.pickResultBuf);
  drv.glGetBufferSubData(eGL_COPY_READ_BUFFER, 0, sizeof(uint32_t), &numResults);

  uint32_t ret = ~0U;

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

      byte *mapped = (byte *)drv.glMapNamedBufferEXT(DebugData.pickResultBuf, eGL_READ_ONLY);

      mapped += sizeof(uint32_t) * 4;

      PickResult *pickResults = (PickResult *)mapped;

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

      // min with size of results buffer to protect against overflows
      for(uint32_t i = 1; i < RDCMIN((uint32_t)DebugRenderData::maxMeshPicks, numResults); i++)
      {
        float pickDistance = (pickResults[i].intersectionPoint - rayPos).Length();
        if(pickDistance < closestPickDistance)
        {
          closest = pickResults + i;
        }
      }

      drv.glUnmapNamedBufferEXT(DebugData.pickResultBuf);

      ret = closest->vertid;
    }
    else
    {
      struct PickResult
      {
        uint32_t vertid;
        uint32_t idx;
        float len;
        float depth;
      };

      byte *mapped = (byte *)drv.glMapNamedBufferEXT(DebugData.pickResultBuf, eGL_READ_ONLY);

      mapped += sizeof(uint32_t) * 4;

      PickResult *pickResults = (PickResult *)mapped;

      PickResult *closest = pickResults;

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

      drv.glUnmapNamedBufferEXT(DebugData.pickResultBuf);

      ret = closest->vertid;
    }
  }

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

  return ret;
}

void GLReplay::RenderCheckerboard(FloatVector dark, FloatVector light)
{
  MakeCurrentReplayContext(m_DebugCtx);

  WrappedOpenGL &drv = *m_pDriver;

  drv.glUseProgram(DebugData.checkerProg);

  drv.glDisable(eGL_DEPTH_TEST);

  if(HasExt[EXT_framebuffer_sRGB])
    drv.glEnable(eGL_FRAMEBUFFER_SRGB);

  drv.glBindBufferBase(eGL_UNIFORM_BUFFER, 0, DebugData.UBOs[0]);

  CheckerboardUBOData *ubo =
      (CheckerboardUBOData *)drv.glMapBufferRange(eGL_UNIFORM_BUFFER, 0, sizeof(CheckerboardUBOData),
                                                  GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);

  if(!ubo)
  {
    RDCERR("Map buffer failed %d", drv.glGetError());
    return;
  }

  ubo->BorderWidth = 0.0f;
  ubo->RectPosition = Vec2f();
  ubo->RectSize = Vec2f();
  ubo->CheckerSquareDimension = 64.0f;
  ubo->InnerColor = Vec4f();

  ubo->PrimaryColor = ConvertSRGBToLinear(dark);
  ubo->SecondaryColor = ConvertSRGBToLinear(light);

  drv.glUnmapBuffer(eGL_UNIFORM_BUFFER);

  drv.glBindVertexArray(DebugData.emptyVAO);
  drv.glDrawArrays(eGL_TRIANGLE_STRIP, 0, 4);
}

void GLReplay::RenderHighlightBox(float w, float h, float scale)
{
  MakeCurrentReplayContext(m_DebugCtx);

  WrappedOpenGL &drv = *m_pDriver;

  GLint sz = GLint(scale);

  struct rect
  {
    GLint x, y;
    GLint w, h;
  };

  rect tl = {GLint(w / 2.0f + 0.5f), GLint(h / 2.0f + 0.5f), 1, 1};

  rect scissors[4] = {
      {tl.x, tl.y - (GLint)sz - 1, 1, sz + 1},
      {tl.x + (GLint)sz, tl.y - (GLint)sz - 1, 1, sz + 2},
      {tl.x, tl.y, sz, 1},
      {tl.x, tl.y - (GLint)sz - 1, sz, 1},
  };

  // inner
  drv.glEnable(eGL_SCISSOR_TEST);
  drv.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  for(size_t i = 0; i < ARRAY_COUNT(scissors); i++)
  {
    drv.glScissor(scissors[i].x, scissors[i].y, scissors[i].w, scissors[i].h);
    drv.glClear(eGL_COLOR_BUFFER_BIT);
  }

  scissors[0].x--;
  scissors[1].x++;
  scissors[2].x--;
  scissors[3].x--;

  scissors[0].y--;
  scissors[1].y--;
  scissors[2].y++;
  scissors[3].y--;

  scissors[0].h += 2;
  scissors[1].h += 2;
  scissors[2].w += 2;
  scissors[3].w += 2;

  // outer
  drv.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  for(size_t i = 0; i < ARRAY_COUNT(scissors); i++)
  {
    drv.glScissor(scissors[i].x, scissors[i].y, scissors[i].w, scissors[i].h);
    drv.glClear(eGL_COLOR_BUFFER_BIT);
  }

  drv.glDisable(eGL_SCISSOR_TEST);
}
