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

#include "gl_test.h"

using namespace TextureZoo;

RD_TEST(GL_Texture_Zoo, OpenGLGraphicsTest)
{
  static constexpr const char *Description =
      "Tests all possible combinations of texture type and format that are supported.";

  std::string blitVertex = R"EOSHADER(
#version 420 core

void main()
{
  const vec4 verts[4] = vec4[4](vec4(-1.0, -1.0, 0.5, 1.0), vec4(3.0, -1.0, 0.5, 1.0),
                                vec4(-1.0, 3.0, 0.5, 1.0), vec4(1.0, 1.0, 0.5, 1.0));

  gl_Position = verts[gl_VertexID];
}

)EOSHADER";

  std::string pixelTemplate = R"EOSHADER(
#version 420 core

layout(binding = 0) uniform &texdecl intex;

layout(location = 0, index = 0) out vec4 Color;

vec4 cubeFetch(samplerCube t, int i)
{
  return textureLod(t, vec3(1,0,0), 0.0);
}

vec4 cubeFetch(samplerCubeArray t, int i)
{
  return textureLod(t, vec4(1,0,0,0), 0.0);
}

vec4 cubeFetch(usamplerCube t, int i)
{
  return textureLod(t, vec3(1,0,0), 0.0);
}

vec4 cubeFetch(usamplerCubeArray t, int i)
{
  return textureLod(t, vec4(1,0,0,0), 0.0);
}

vec4 cubeFetch(isamplerCube t, int i)
{
  return textureLod(t, vec3(1,0,0), 0.0);
}

vec4 cubeFetch(isamplerCubeArray t, int i)
{
  return textureLod(t, vec4(1,0,0,0), 0.0);
}

void main()
{
	Color = vec4(texelFetch(intex, &params));
}
)EOSHADER";

  std::string pixelMSFloat = R"EOSHADER(
#version 420 core

uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;

float srgb2linear(float f)
{
  if (f <= 0.04045f)
    return f / 12.92f;
  else
    return pow((f + 0.055f) / 1.055f, 2.4f);
}

layout(location = 0, index = 0) out vec4 Color;

void main()
{
  uint x = uint(gl_FragCoord.x);
  uint y = uint(gl_FragCoord.y);

  vec4 ret = vec4(0.1f, 0.35f, 0.6f, 0.85f);

  // each 3D slice cycles the x. This only affects the primary diagonal
  uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);

  // pixels off the diagonal invert the colors
  if(offs_x != y)
    ret = ret.wzyx;

  // second slice adds a coarse checkerboard pattern of inversion
  if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
    ret = ret.wzyx;

  // second sample/mip is shifted up a bit. MSAA textures have no mips,
  // textures with mips have no samples.
  ret += 0.075f.xxxx * (gl_SampleID + mip);

  // Signed normals are negative
  if((flags & 1) != 0)
    ret = -ret;

  // undo SRGB curve applied in output merger, to match the textures we just blat values into
  // without conversion (which are then interpreted as srgb implicitly)
  if((flags & 2) != 0)
  {
    ret.r = srgb2linear(ret.r);
    ret.g = srgb2linear(ret.g);
    ret.b = srgb2linear(ret.b);
  }

  // BGR flip - same as above, for BGRA textures
  if((flags & 4) != 0)
    ret.rgb = ret.bgr;

   // put red into alpha, because that's what we did in manual upload
  if((flags & 8) != 0)
    ret.a = ret.r;

  Color = ret;
}

)EOSHADER";

  std::string pixelMSDepth = R"EOSHADER(
#version 420 core

uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;

void main()
{
  uint x = uint(gl_FragCoord.x);
  uint y = uint(gl_FragCoord.y);

  float ret = 0.1f;

  // each 3D slice cycles the x. This only affects the primary diagonal
  uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);

  // pixels off the diagonal invert the colors
  // second slice adds a coarse checkerboard pattern of inversion
  if((offs_x != y) != (slice > 0 && (((x / 2) % 2) != ((y / 2) % 2))))
  {
    ret = 0.85f;

    // so we can fill stencil data, clip off the inverted values
    if(flags == 1)
      discard;
  }

  // second sample/mip is shifted up a bit. MSAA textures have no mips,
  // textures with mips have no samples.
  ret += 0.075f * (gl_SampleID + mip);

  gl_FragDepth = ret;
}

)EOSHADER";

  std::string pixelMSUInt = R"EOSHADER(
#version 420 core

uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;

layout(location = 0, index = 0) out uvec4 Color;

void main()
{
  uint x = uint(gl_FragCoord.x);
  uint y = uint(gl_FragCoord.y);

  uvec4 ret = uvec4(10, 40, 70, 100);

  // each 3D slice cycles the x. This only affects the primary diagonal
  uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);

  // pixels off the diagonal invert the colors
  if(offs_x != y)
    ret = ret.wzyx;

  // second slice adds a coarse checkerboard pattern of inversion
  if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
    ret = ret.wzyx;

  // second sample/mip is shifted up a bit. MSAA textures have no mips,
  // textures with mips have no samples.
  ret += uvec4(10, 10, 10, 10) * (gl_SampleID + mip);

  Color = ret;
}

)EOSHADER";

  std::string pixelMSSInt = R"EOSHADER(
#version 420 core

uniform uint texWidth;
uniform uint slice;
uniform uint mip;
uniform uint flags;
uniform uint zlayer;

layout(location = 0, index = 0) out ivec4 Color;

void main()
{
  uint x = uint(gl_FragCoord.x);
  uint y = uint(gl_FragCoord.y);

  ivec4 ret = ivec4(10, 40, 70, 100);

  // each 3D slice cycles the x. This only affects the primary diagonal
  uint offs_x = (x + zlayer) % max(1u, texWidth >> mip);

  // pixels off the diagonal invert the colors
  if(offs_x != y)
    ret = ret.wzyx;

  // second slice adds a coarse checkerboard pattern of inversion
  if(slice > 0 && (((x / 2) % 2) != ((y / 2) % 2)))
    ret = ret.wzyx;

  // second sample/mip is shifted up a bit. MSAA textures have no mips,
  // textures with mips have no samples.
  ret += ivec4(10 * (gl_SampleID + mip));

  Color = -ret;
}

)EOSHADER";

  struct GLFormat
  {
    const std::string name;
    GLenum internalFormat;
    TexConfig cfg;
  };

  struct TestCase
  {
    GLFormat fmt;
    GLenum target;
    uint32_t dim;
    bool isArray;
    bool isMSAA;
    bool isRect;
    bool isCube;
    bool canRender;
    bool canDepth;
    bool canStencil;
    bool hasData;
    GLuint tex;
  };

  std::string MakeName(const TestCase &test)
  {
    std::string name = "Texture " + std::to_string(test.dim) + "D";

    if(test.isRect)
      name = "Texture Rect";
    if(test.isCube)
      name = "Texture Cube";

    if(test.isMSAA)
      name += " MSAA";
    if(test.isArray)
      name += " Array";

    return name;
  }

  GLuint GetProgram(const TestCase &test)
  {
    static std::map<uint32_t, GLuint> programs;

    uint32_t key = uint32_t(test.fmt.cfg.data);
    key |= test.dim << 6;
    key |= test.isMSAA ? 0x80000 : 0;
    key |= test.isArray ? 0x100000 : 0;
    key |= test.isRect ? 0x200000 : 0;
    key |= test.isCube ? 0x400000 : 0;

    GLuint ret = programs[key];
    if(!ret)
    {
      std::string texType = "sampler" + std::to_string(test.dim) + "D";
      if(test.isMSAA)
        texType += "MS";
      if(test.isRect)
        texType += "Rect";
      if(test.dim < 3 && test.isArray)
        texType += "Array";

      if(test.isCube)
      {
        texType = "samplerCube";
        if(test.isArray)
          texType += "Array";
      }

      std::string typemod = "";

      if(test.fmt.cfg.data == DataType::UInt)
        typemod = "u";
      else if(test.fmt.cfg.data == DataType::SInt)
        typemod = "i";

      std::string src = pixelTemplate;

      uint32_t dim = test.dim + (test.isArray ? 1 : 0);

      if(test.isCube)
      {
        src.replace(src.find("&params"), 7, "int(0)");
        src.replace(src.find("texelFetch"), 10, "cubeFetch");
      }
      else if(test.isRect)
      {
        src.replace(src.find("&params"), 7, "ivec2(0)");
      }
      else if(dim == 1)
      {
        src.replace(src.find("&params"), 7, "int(0), 0");
      }
      else if(dim == 2)
      {
        src.replace(src.find("&params"), 7, "ivec2(0), 0");
      }
      else if(dim == 3)
      {
        src.replace(src.find("&params"), 7, "ivec3(0), 0");
      }

      src.replace(src.find("&texdecl"), 8, typemod + texType);

      ret = programs[key] = MakeProgram(blitVertex, src);
    }

    return ret;
  }

  bool QueryFormatBool(GLenum target, GLenum format, GLenum pname)
  {
    GLint param = 0;
    glGetInternalformativ(target, format, pname, 1, &param);
    return param != 0;
  }

  void FinaliseTest(TestCase & test)
  {
    test.canRender = QueryFormatBool(test.target, test.fmt.internalFormat, GL_COLOR_RENDERABLE);
    test.canDepth = QueryFormatBool(test.target, test.fmt.internalFormat, GL_DEPTH_RENDERABLE);
    test.canStencil = QueryFormatBool(test.target, test.fmt.internalFormat, GL_STENCIL_RENDERABLE);

    GLint numSamples = 0;
    glGetInternalformativ(test.target, test.fmt.internalFormat, GL_NUM_SAMPLE_COUNTS, 1, &numSamples);

    GLint samples[8];
    glGetInternalformativ(test.target, test.fmt.internalFormat, GL_SAMPLES, 1, samples);

    Vec4i dimensions(texWidth, texHeight, texDepth);

    bool isCompressed =
        (test.fmt.cfg.type != TextureType::R9G9B9E5 && test.fmt.cfg.type != TextureType::Regular);

    // Some GL drivers report that block compressed textures are supported for MSAA and color
    // rendering. Save them from themselves. Similarly they report support for 1D and 3D but then it
    // doesn't work properly
    if(isCompressed && (test.dim == 1 || test.dim == 3 || test.isRect || test.isCube || test.isMSAA))
      return;

    // don't create integer cubemaps, or non-regular format cubemaps
    if(test.isCube && (test.fmt.cfg.type != TextureType::Regular ||
                       test.fmt.cfg.data == DataType::SInt || test.fmt.cfg.data == DataType::UInt))
      return;

    // if the format is MSAA check we have our sample count
    if(test.isMSAA)
    {
      bool found = false;
      for(GLint i = 0; i < numSamples; i++)
        found |= (samples[i] == texSamples);

      if(!found)
        return;
    }

    // any format that supports MSAA but can't be rendered to is unsupported
    if(!test.canRender && !test.canDepth && !test.canStencil && test.isMSAA)
      return;

    test.tex = MakeTexture();
    glBindTexture(test.target, test.tex);

    // MSAA textures don't have sampler state at all
    if(!test.isMSAA)
    {
      // rectangle textures can't have mipmap minification
      glTexParameteri(test.target, GL_TEXTURE_MIN_FILTER,
                      test.isRect ? GL_NEAREST : GL_NEAREST_MIPMAP_NEAREST);
      glTexParameteri(test.target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }

    if(test.dim == 1)
    {
      if(test.isArray)
        glTexStorage2D(test.target, texMips, test.fmt.internalFormat, texWidth, texSlices);
      else
        glTexStorage1D(test.target, texMips, test.fmt.internalFormat, texWidth);

      dimensions.y = dimensions.z = 1;
    }
    else if(test.isRect)
    {
      glTexStorage2D(test.target, 1, test.fmt.internalFormat, texWidth, texHeight);

      dimensions.z = 1;
    }
    else if(test.isCube)
    {
      if(test.isArray)
        glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, texMips, test.fmt.internalFormat, texWidth,
                       texHeight, 12);
      else
        glTexStorage2D(GL_TEXTURE_CUBE_MAP, texMips, test.fmt.internalFormat, texWidth, texHeight);

      dimensions.z = 1;
    }
    else if(test.dim == 2)
    {
      if(test.isMSAA)
      {
        if(test.isArray)
          glTexStorage3DMultisample(test.target, texSamples, test.fmt.internalFormat, texWidth,
                                    texHeight, texSlices, GL_TRUE);
        else
          glTexStorage2DMultisample(test.target, texSamples, test.fmt.internalFormat, texWidth,
                                    texHeight, GL_TRUE);
      }
      else
      {
        if(test.isArray)
          glTexStorage3D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight,
                         texSlices);
        else
          glTexStorage2D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight);
      }

      dimensions.z = 1;
    }
    else if(test.dim == 3)
    {
      glTexStorage3D(test.target, texMips, test.fmt.internalFormat, texWidth, texHeight, texDepth);
    }

    if(test.canRender || test.canDepth || test.canStencil)
    {
      glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
      glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, 0, 0);

      GLenum attach = GL_COLOR_ATTACHMENT0;
      if(test.canDepth && test.canStencil)
        attach = GL_DEPTH_STENCIL_ATTACHMENT;
      else if(test.canDepth)
        attach = GL_DEPTH_ATTACHMENT;
      else if(test.canStencil)
        attach = GL_STENCIL_ATTACHMENT;

      if(test.dim == 3 || test.isArray)
        glFramebufferTextureLayer(GL_FRAMEBUFFER, attach, test.tex, 0, 0);
      else
        glFramebufferTexture(GL_FRAMEBUFFER, attach, test.tex, 0);

      GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);

      // sometimes the GL driver lies about being able to render to a format! weeee!
      if(status != GL_FRAMEBUFFER_COMPLETE)
      {
        test.canRender = false;
        test.canDepth = false;
        test.canStencil = false;
      }

      if(!test.canRender && !test.canDepth && !test.canStencil && test.isMSAA)
      {
        test.tex = 0;
        return;
      }
    }

    glObjectLabel(GL_TEXTURE, test.tex, -1, (MakeName(test) + " " + test.fmt.name).c_str());

    // invalidate the texture, this makes renderdoc treat it as dirty
    glInvalidateTexImage(test.tex, 0);

    if(!test.isMSAA)
    {
      pushMarker("Set data for " + test.fmt.name + " " + MakeName(test));

      test.hasData = SetData(test, dimensions);

      popMarker();
    }
  }

  bool SetData(const TestCase &test, Vec4i dim)
  {
    bool isCompressed =
        (test.fmt.cfg.type != TextureType::R9G9B9E5 && test.fmt.cfg.type != TextureType::Regular);

    TexData data;

    // tightly packed data
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);

    GLenum format = GL_RGBA;
    GLenum type;

    if(test.fmt.cfg.data == DataType::UInt || test.fmt.cfg.data == DataType::UNorm)
    {
      type = GL_UNSIGNED_BYTE;
      if(test.fmt.cfg.componentBytes == 2)
        type = GL_UNSIGNED_SHORT;
      else if(test.fmt.cfg.componentBytes == 4)
        type = GL_UNSIGNED_INT;
    }
    else if(test.fmt.cfg.data == DataType::SInt || test.fmt.cfg.data == DataType::SNorm)
    {
      type = GL_BYTE;
      if(test.fmt.cfg.componentBytes == 2)
        type = GL_SHORT;
      else if(test.fmt.cfg.componentBytes == 4)
        type = GL_INT;
    }
    else
    {
      type = GL_FLOAT;
      if(test.fmt.cfg.componentBytes == 2)
        type = GL_HALF_FLOAT;
    }

    bool isInt = (test.fmt.cfg.data == DataType::SInt || test.fmt.cfg.data == DataType::UInt);
    if(test.fmt.cfg.componentCount == 4)
      format = isInt ? GL_RGBA_INTEGER : GL_RGBA;
    else if(test.fmt.cfg.componentCount == 3)
      format = isInt ? GL_RGB_INTEGER : GL_RGB;
    else if(test.fmt.cfg.componentCount == 2)
      format = isInt ? GL_RG_INTEGER : GL_RG;
    else if(test.fmt.cfg.componentCount == 1)
      format = isInt ? GL_RED_INTEGER : GL_RED;

    if(test.fmt.cfg.type == TextureType::R9G9B9E5)
    {
      format = GL_RGB;
      type = GL_UNSIGNED_INT_5_9_9_9_REV;
    }

    GLint slices = test.isArray ? texSlices : 1;
    GLint mips = test.isMSAA || test.isRect ? 1 : texMips;

    if(test.isCube)
      slices = test.isArray ? 12 : 6;

    for(GLint s = 0; s < slices; s++)
    {
      for(GLint m = 0; m < mips; m++)
      {
        MakeData(data, test.fmt.cfg, dim, m, s);

        if(data.byteData.empty())
          return false;

        uint32_t mipWidth = std::max(texWidth >> m, 1U);
        uint32_t mipHeight = std::max(texHeight >> m, 1U);
        uint32_t mipDepth = std::max(texDepth >> m, 1U);

        if(isCompressed)
        {
          if(test.dim == 1)
          {
            if(test.isArray)
              glCompressedTexSubImage2D(test.target, m, 0, s, mipWidth, 1, test.fmt.internalFormat,
                                        (GLsizei)data.byteData.size(), data.byteData.data());
            else
              glCompressedTexSubImage1D(test.target, m, 0, mipWidth, format,
                                        (GLsizei)data.byteData.size(), data.byteData.data());
          }
          else if(test.isRect)
          {
            glCompressedTexSubImage2D(test.target, 0, 0, 0, mipWidth, mipHeight,
                                      test.fmt.internalFormat, (GLsizei)data.byteData.size(),
                                      data.byteData.data());
          }
          else if(test.dim == 2)
          {
            if(test.isArray)
              glCompressedTexSubImage3D(test.target, m, 0, 0, s, mipWidth, mipHeight, 1,
                                        test.fmt.internalFormat, (GLsizei)data.byteData.size(),
                                        data.byteData.data());
            else
              glCompressedTexSubImage2D(test.target, m, 0, 0, mipWidth, mipHeight,
                                        test.fmt.internalFormat, (GLsizei)data.byteData.size(),
                                        data.byteData.data());
          }
          else if(test.dim == 3)
          {
            glCompressedTexSubImage3D(test.target, m, 0, 0, 0, mipWidth, mipHeight, mipDepth,
                                      test.fmt.internalFormat, (GLsizei)data.byteData.size(),
                                      data.byteData.data());
          }
        }
        else
        {
          if(test.dim == 1)
          {
            if(test.isArray)
              glTexSubImage2D(test.target, m, 0, s, mipWidth, 1, format, type, data.byteData.data());
            else
              glTexSubImage1D(test.target, m, 0, mipWidth, format, type, data.byteData.data());
          }
          else if(test.isRect)
          {
            glTexSubImage2D(test.target, 0, 0, 0, mipWidth, mipHeight, format, type,
                            data.byteData.data());
          }
          else if(test.isCube)
          {
            if(test.isArray)
            {
              glTexSubImage3D(test.target, m, 0, 0, s, mipWidth, mipHeight, 1, format, type,
                              data.byteData.data());
            }
            else
            {
              GLenum faces[] = {
                  GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
                  GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
                  GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
              };

              glTexSubImage2D(faces[s], m, 0, 0, mipWidth, mipHeight, format, type,
                              data.byteData.data());
            }
          }
          else if(test.dim == 2)
          {
            if(test.isArray)
              glTexSubImage3D(test.target, m, 0, 0, s, mipWidth, mipHeight, 1, format, type,
                              data.byteData.data());
            else
              glTexSubImage2D(test.target, m, 0, 0, mipWidth, mipHeight, format, type,
                              data.byteData.data());
          }
          else if(test.dim == 3)
          {
            glTexSubImage3D(test.target, m, 0, 0, 0, mipWidth, mipHeight, mipDepth, format, type,
                            data.byteData.data());
          }
        }
      }
    }

    return true;
  }

  void AddSupportedTests(const GLFormat &f, std::vector<TestCase> &test_textures, bool depthMode)
  {
    // TODO: disable 1D depth textures for now, we don't support displaying them
    if(!depthMode)
    {
      test_textures.push_back({f, GL_TEXTURE_1D, 1, false});
      test_textures.push_back({f, GL_TEXTURE_1D_ARRAY, 1, true});
    }

    test_textures.push_back({f, GL_TEXTURE_2D, 2, false});
    test_textures.push_back({f, GL_TEXTURE_2D_ARRAY, 2, true});

    test_textures.push_back({f, GL_TEXTURE_3D, 3, false});

    // TODO: we don't support MSAA<->Array copies for these odd sized pixels, and I suspect drivers
    // emulate the formats anyway. Disable for now
    if(f.cfg.type != TextureType::Regular || f.cfg.componentCount != 3)
    {
      test_textures.push_back({f, GL_TEXTURE_2D_MULTISAMPLE, 2, false, true});
      test_textures.push_back({f, GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 2, true, true});
    }

    test_textures.push_back({f, GL_TEXTURE_RECTANGLE, 2, false, false, true});

    if(!depthMode)
    {
      test_textures.push_back({f, GL_TEXTURE_CUBE_MAP, 2, false, false, false, true});
      test_textures.push_back({f, GL_TEXTURE_CUBE_MAP_ARRAY, 2, true, false, false, true});
    }
  }

  int main()
  {
    // initialise, create window, create context, etc
    if(!Init())
      return 3;

    GLuint vao = MakeVAO();
    glBindVertexArray(vao);

    pushMarker("Add tests");

#define TEST_CASE_NAME(texFmt) #texFmt

#define TEST_CASE(texType, texFmt, compCount, byteWidth, dataType)    \
  {                                                                   \
    &(#texFmt)[3], texFmt, {texType, compCount, byteWidth, dataType}, \
  }

    std::vector<TestCase> test_textures;

    const GLFormat color_tests[] = {
        TEST_CASE(TextureType::Regular, GL_RGBA32F, 4, 4, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RGBA32UI, 4, 4, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGBA32I, 4, 4, DataType::SInt),

        TEST_CASE(TextureType::Regular, GL_RGB32F, 3, 4, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RGB32UI, 3, 4, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGB32I, 3, 4, DataType::SInt),

        TEST_CASE(TextureType::Regular, GL_RG32F, 2, 4, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RG32UI, 2, 4, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RG32I, 2, 4, DataType::SInt),

        TEST_CASE(TextureType::Regular, GL_R32F, 1, 4, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_R32UI, 1, 4, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_R32I, 1, 4, DataType::SInt),

        TEST_CASE(TextureType::Regular, GL_RGBA16F, 4, 2, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RGBA16UI, 4, 2, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGBA16I, 4, 2, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RGBA16, 4, 2, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RGBA16_SNORM, 4, 2, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_RGB16F, 3, 2, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RGB16UI, 3, 2, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGB16I, 3, 2, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RGB16, 3, 2, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RGB16_SNORM, 3, 2, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_RG16F, 2, 2, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_RG16UI, 2, 2, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RG16I, 2, 2, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RG16, 2, 2, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RG16_SNORM, 2, 2, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_R16F, 1, 2, DataType::Float),
        TEST_CASE(TextureType::Regular, GL_R16UI, 1, 2, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_R16I, 1, 2, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_R16, 1, 2, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_R16_SNORM, 1, 2, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_RGBA8UI, 4, 1, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGBA8I, 4, 1, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RGBA8, 4, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_SRGB8_ALPHA8, 4, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RGBA8_SNORM, 4, 1, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_RGB8UI, 3, 1, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RGB8I, 3, 1, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RGB8, 3, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_SRGB8, 3, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RGB8_SNORM, 3, 1, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_RG8UI, 2, 1, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_RG8I, 2, 1, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_RG8, 2, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_SRG8_EXT, 1, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_RG8_SNORM, 2, 1, DataType::SNorm),

        TEST_CASE(TextureType::Regular, GL_R8UI, 1, 1, DataType::UInt),
        TEST_CASE(TextureType::Regular, GL_R8I, 1, 1, DataType::SInt),
        TEST_CASE(TextureType::Regular, GL_R8, 1, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_SR8_EXT, 1, 1, DataType::UNorm),
        TEST_CASE(TextureType::Regular, GL_R8_SNORM, 1, 1, DataType::SNorm),

        TEST_CASE(TextureType::Unknown, GL_RGB565, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::Unknown, GL_RGB5_A1, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::Unknown, GL_RGB10_A2, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::Unknown, GL_RGB10_A2UI, 0, 0, DataType::UInt),

        TEST_CASE(TextureType::Unknown, GL_RGBA4, 0, 0, DataType::UNorm),

        // formats we don't support in RenderDoc currently
        // TEST_CASE(TextureType::Unknown, GL_RGB4, 0, 0, DataType::UNorm),
        // TEST_CASE(TextureType::Unknown, GL_RGB5, 0, 0, DataType::UNorm),
        // TEST_CASE(TextureType::Unknown, GL_RGB10, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::Unknown, GL_R11F_G11F_B10F, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::R9G9B9E5, GL_RGB9_E5, 0, 0, DataType::Float),

        TEST_CASE(TextureType::BC1, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC1, GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC1, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC1, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::BC2, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC2, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::BC3, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC3, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, 0, 0, DataType::UNorm),

        TEST_CASE(TextureType::BC4, GL_COMPRESSED_RED_RGTC1_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC4, GL_COMPRESSED_SIGNED_RED_RGTC1_EXT, 0, 0, DataType::SNorm),

        TEST_CASE(TextureType::BC5, GL_COMPRESSED_RED_GREEN_RGTC2_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC5, GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT, 0, 0, DataType::SNorm),

        TEST_CASE(TextureType::BC6, GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC6, GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT, 0, 0, DataType::SNorm),

        TEST_CASE(TextureType::BC7, GL_COMPRESSED_RGBA_BPTC_UNORM, 0, 0, DataType::UNorm),
        TEST_CASE(TextureType::BC7, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, 0, 0, DataType::UNorm),
    };

    for(GLFormat f : color_tests)
    {
      if(f.internalFormat == GL_SR8_EXT && !GLAD_GL_EXT_texture_sRGB_R8)
        continue;

      if(f.internalFormat == GL_SRG8_EXT && !GLAD_GL_EXT_texture_sRGB_RG8)
        continue;

      AddSupportedTests(f, test_textures, false);
    }

    // finally add the depth tests
    const GLFormat depth_tests[] = {
        TEST_CASE(TextureType::Unknown, GL_DEPTH32F_STENCIL8, 0, 0, DataType::Float),
        TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT32F, 0, 0, DataType::Float),

        TEST_CASE(TextureType::Unknown, GL_DEPTH24_STENCIL8, 0, 0, DataType::Float),
        TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT24, 0, 0, DataType::Float),

        TEST_CASE(TextureType::Unknown, GL_DEPTH_COMPONENT16, 0, 0, DataType::Float),
    };

    for(GLFormat f : depth_tests)
      AddSupportedTests(f, test_textures, true);

    GLuint renderFBO = MakeFBO();
    glBindFramebuffer(GL_FRAMEBUFFER, renderFBO);

    for(TestCase &t : test_textures)
    {
      if(QueryFormatBool(t.target, t.fmt.internalFormat, GL_INTERNALFORMAT_SUPPORTED) &&
         QueryFormatBool(t.target, t.fmt.internalFormat, GL_FRAGMENT_TEXTURE))
      {
        FinaliseTest(t);
      }
    }

    popMarker();

    GLuint msprog[(size_t)DataType::Count];

    msprog[(size_t)DataType::Float] = msprog[(size_t)DataType::UNorm] =
        msprog[(size_t)DataType::SNorm] = MakeProgram(blitVertex, pixelMSFloat);
    msprog[(size_t)DataType::UInt] = MakeProgram(blitVertex, pixelMSUInt);
    msprog[(size_t)DataType::SInt] = MakeProgram(blitVertex, pixelMSSInt);

    GLuint msdepthprog = MakeProgram(blitVertex, pixelMSDepth);

    for(TestCase &t : test_textures)
    {
      if(!t.tex || t.hasData)
        continue;

      if(!t.canRender && !t.canDepth && !t.canStencil)
      {
        TEST_ERROR("Need data for test %s, but it's not a renderable/depthable format",
                   t.fmt.name.c_str());
        continue;
      }

      glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
      glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, 0, 0);

      if(t.canDepth || t.canStencil)
      {
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_STENCIL_TEST);
        glDepthMask(0xff);
        glDepthFunc(GL_ALWAYS);
        glStencilFunc(GL_ALWAYS, 0, 0xff);
        glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
      }
      else
      {
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_STENCIL_TEST);
      }

      pushMarker("Render data for " + t.fmt.name + " " + MakeName(t));

      t.hasData = true;

      bool srgb = false;
      bool bgr = false;
      switch(t.fmt.internalFormat)
      {
        // only need to handle renderable SRGB formats here
        case GL_SRGB8:
        case GL_SRGB8_ALPHA8: srgb = true; break;
        case GL_BGRA8_EXT:
        case GL_RGBA4:
        case GL_RGB5_A1:
        case GL_RGB565: bgr = true;
        default: break;
      }

      int flags = 0;

      if(t.fmt.cfg.data == DataType::SNorm)
        flags |= 1;
      if(srgb)
        flags |= 2;
      if(bgr)
        flags |= 4;

      GLuint slices = t.isArray ? texSlices : 1u;
      GLuint mips = t.isMSAA || t.isRect ? 1u : texMips;

      for(GLuint mp = 0; mp < mips; mp++)
      {
        GLuint SlicesOrDepth = slices;
        if(t.dim == 3)
          SlicesOrDepth >>= mp;

        for(GLuint sl = 0; sl < SlicesOrDepth; sl++)
        {
          if(t.canDepth || t.canStencil)
          {
            GLenum attach = GL_NONE;
            if(t.canDepth && t.canStencil)
              attach = GL_DEPTH_STENCIL_ATTACHMENT;
            else if(t.canDepth)
              attach = GL_DEPTH_ATTACHMENT;
            else if(t.canStencil)
              attach = GL_STENCIL_ATTACHMENT;

            if(t.dim == 3 || t.isArray)
              glFramebufferTextureLayer(GL_FRAMEBUFFER, attach, t.tex, mp, sl);
            else
              glFramebufferTexture(GL_FRAMEBUFFER, attach, t.tex, mp);

            glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.0, 0);

            GLuint p = msdepthprog;

            glUseProgram(p);

            glUniform1ui(glGetUniformLocation(p, "texWidth"), texWidth);

            glUniform1ui(glGetUniformLocation(p, "slice"), t.dim == 3 ? 0 : sl);
            glUniform1ui(glGetUniformLocation(p, "mip"), mp);
            glUniform1ui(glGetUniformLocation(p, "flags"), flags);
            glUniform1ui(glGetUniformLocation(p, "zlayer"), t.dim == 3 ? sl : 0);

            glViewport(0, 0, texWidth, texHeight);

            uint32_t sampleCount = t.isMSAA ? texSamples : 1;

            // need to do each sample separately to let us vary the stencil value
            for(uint32_t sm = 0; sm < sampleCount; sm++)
            {
              glSampleMaski(0, 1 << sm);

              glStencilFunc(GL_ALWAYS, 100 + (mp + sm) * 10, 0xff);

              glDrawArrays(GL_TRIANGLES, 0, 3);

              // clip off the diagonal
              glUniform1ui(glGetUniformLocation(p, "flags"), 1);

              glStencilFunc(GL_ALWAYS, 10 + (mp + sm) * 10, 0xff);

              glDrawArrays(GL_TRIANGLES, 0, 3);
            }
          }
          else
          {
            if(t.dim == 3 || t.isArray)
              glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, t.tex, mp, sl);
            else
              glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, t.tex, mp);

            GLuint p = msprog[(size_t)t.fmt.cfg.data];

            glUseProgram(p);

            glUniform1ui(glGetUniformLocation(p, "texWidth"), texWidth);

            glUniform1ui(glGetUniformLocation(p, "slice"), t.dim == 3 ? 0 : sl);
            glUniform1ui(glGetUniformLocation(p, "mip"), mp);
            glUniform1ui(glGetUniformLocation(p, "flags"), flags);
            glUniform1ui(glGetUniformLocation(p, "zlayer"), t.dim == 3 ? sl : 0);

            glViewport(0, 0, texWidth, texHeight);

            glDrawArrays(GL_TRIANGLES, 0, 3);
          }
        }
      }

      popMarker();
    }

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_STENCIL_TEST);

    GLuint fbo = MakeFBO();
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    // Color render texture
    GLuint colattach = MakeTexture();
    glBindTexture(GL_TEXTURE_2D, colattach);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, screenWidth, screenHeight);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colattach, 0);

    std::vector<Vec4f> blue;
    blue.resize(64 * 64 * 64, Vec4f(0.0f, 0.0f, 1.0f, 1.0f));

    std::vector<Vec4f> green;
    green.resize(64 * 64, Vec4f(0.0f, 1.0f, 0.0f, 1.0f));

    // slice testing textures

    TestCase slice_test_array = {};
    TestCase slice_test_3d = {};
    slice_test_array.tex = MakeTexture();
    slice_test_array.dim = 2;
    slice_test_array.isArray = true;
    glBindTexture(GL_TEXTURE_2D_ARRAY, slice_test_array.tex);
    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 2, GL_RGBA32F, 64, 64, 64);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 64, 64, 64, GL_RGBA, GL_FLOAT, blue.data());
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 32, 32, 32, GL_RGBA, GL_FLOAT, blue.data());
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 17, 64, 64, 1, GL_RGBA, GL_FLOAT, green.data());
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 17, 32, 32, 1, GL_RGBA, GL_FLOAT, green.data());

    slice_test_3d.tex = MakeTexture();
    slice_test_3d.dim = 3;
    glBindTexture(GL_TEXTURE_3D, slice_test_3d.tex);
    glTexStorage3D(GL_TEXTURE_3D, 2, GL_RGBA32F, 64, 64, 64);
    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 64, 64, 64, GL_RGBA, GL_FLOAT, blue.data());
    glTexSubImage3D(GL_TEXTURE_3D, 1, 0, 0, 0, 32, 32, 32, GL_RGBA, GL_FLOAT, blue.data());
    glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 17, 64, 64, 1, GL_RGBA, GL_FLOAT, green.data());
    glTexSubImage3D(GL_TEXTURE_3D, 1, 0, 0, 17, 32, 32, 1, GL_RGBA, GL_FLOAT, green.data());

    while(Running())
    {
      glBindFramebuffer(GL_FRAMEBUFFER, fbo);

      float col[] = {0.2f, 0.2f, 0.2f, 1.0f};
      glClearBufferfv(GL_COLOR, 0, col);

      glBindVertexArray(vao);

      GLsizei viewX = 0, viewY = screenHeight - 10;
      glEnable(GL_SCISSOR_TEST);

      // dummy draw for each slice test texture
      pushMarker("slice tests");
      setMarker("2D array");
      glBindTextureUnit(0, slice_test_array.tex);
      glUseProgram(GetProgram(slice_test_array));
      glDrawArrays(GL_TRIANGLES, 0, 0);

      setMarker("3D");
      glBindTextureUnit(0, slice_test_3d.tex);
      glUseProgram(GetProgram(slice_test_3d));
      glDrawArrays(GL_TRIANGLES, 0, 0);
      popMarker();

      for(size_t i = 0; i < test_textures.size(); i++)
      {
        if(i == 0 || test_textures[i].fmt.internalFormat != test_textures[i - 1].fmt.internalFormat)
        {
          if(i != 0)
            popMarker();

          pushMarker(test_textures[i].fmt.name);
        }

        setMarker(MakeName(test_textures[i]));

        glViewport(viewX, viewY, 10, 10);
        glScissor(viewX + 1, viewY + 1, 8, 8);

        glUseProgram(GetProgram(test_textures[i]));

        if(test_textures[i].tex)
        {
          glBindTextureUnit(0, test_textures[i].tex);
          glDrawArrays(GL_TRIANGLES, 0, 3);
        }
        else
        {
          setMarker("UNSUPPORTED");
        }

        // advance to next viewport
        viewX += 10;
        if(viewX + 10 > (float)screenWidth)
        {
          viewX = 0;
          viewY -= 10;
        }
      }

      // pop the last format region
      popMarker();

      glViewport(0, 0, GLsizei(screenWidth), GLsizei(screenHeight));
      glDisable(GL_SCISSOR_TEST);

      // blit to the screen for a nicer preview
      glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

      glBlitFramebuffer(0, 0, screenWidth, screenHeight, 0, 0, screenWidth, screenHeight,
                        GL_COLOR_BUFFER_BIT, GL_NEAREST);

      Present();
    }

    return 0;
  }
};

REGISTER_TEST();
