void APIENTRY _glGetTexImage()

in renderdoc/driver/gl/wrappers/gl_emulated.cpp [2635:3517]


void APIENTRY _glGetTexImage(GLenum target, GLint level, const GLenum format, const GLenum type,
                             void *pixels)
{
  switch(target)
  {
    case eGL_TEXTURE_1D:
    case eGL_TEXTURE_1D_ARRAY:
      RDCWARN("1d and 1d array textures are not supported by GLES");
      return;

    case eGL_TEXTURE_BUFFER:
      // TODO implement this
      GLNOTIMP("Reading pixels from texture buffer");
      return;

    default: break;
  }

  GLuint fbo = 0;
  GL.glGenFramebuffers(1, &fbo);

  PushPopFramebuffer(eGL_FRAMEBUFFER, fbo);

  GLint width = 0, height = 0, depth = 0;
  GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_WIDTH, &width);
  GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_HEIGHT, &height);
  GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_DEPTH, &depth);

  GLenum origInternalFormat = eGL_NONE;
  GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_INTERNAL_FORMAT,
                              (GLint *)&origInternalFormat);

  GLint boundTexture = 0;
  GL.glGetIntegerv(TextureBinding(target), (GLint *)&boundTexture);

  GLuint readtex = boundTexture;
  GLuint deltex = 0;

  GLenum attachment = eGL_COLOR_ATTACHMENT0;
  if(format == eGL_DEPTH_COMPONENT)
    attachment = eGL_DEPTH_ATTACHMENT;
  else if(format == eGL_STENCIL)
    attachment = eGL_STENCIL_ATTACHMENT;
  else if(format == eGL_DEPTH_STENCIL)
    attachment = eGL_DEPTH_STENCIL_ATTACHMENT;

  bool readDirectly = true;
  bool depthFormat = false;

  // we know luminance/alpha formats can't be read directly, so assume failure for them
  if(format == eGL_LUMINANCE_ALPHA || format == eGL_LUMINANCE || format == eGL_ALPHA)
  {
    readDirectly = false;
  }

  // similarly for RGB8, pessimistically assume it can't be read from a framebuffer as not all
  // drivers support it.
  if(format == eGL_RGB && type == eGL_UNSIGNED_BYTE)
  {
    readDirectly = false;
  }

  if((format == eGL_DEPTH_COMPONENT && !HasExt[NV_read_depth]) ||
     (format == eGL_STENCIL && !HasExt[NV_read_stencil]) ||
     (format == eGL_DEPTH_STENCIL && !HasExt[NV_read_depth_stencil]))
  {
    readDirectly = false;
  }

  if(format == eGL_DEPTH_COMPONENT || format == eGL_STENCIL || format == eGL_DEPTH_STENCIL)
  {
    depthFormat = true;
  }

  // Qualcomm drivers seem to barf if we try to read from cubemap faces above X+ for mips 64x64 or
  // smaller. In testing X+ works on any mip, and all faces work on larger mips, but since the
  // driver seems completely unreliable in this area we enable the workaround blanket for all
  // cubemap reads as different formats may break in different ways.
  if(VendorCheck[VendorCheck_Qualcomm_emulate_cube_reads])
  {
    switch(target)
    {
      case eGL_TEXTURE_CUBE_MAP:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_X:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_X:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Z:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
        RDCLOG("Forcing indirect read for cubemap face %s on level %d", ToStr(target).c_str(), level);
        readDirectly = false;
        break;
      default: break;
    }
  }

  // if we can't attach the texture to a framebuffer, we can't readpixels it directly
  if(readDirectly)
  {
    switch(target)
    {
      case eGL_TEXTURE_3D:
      case eGL_TEXTURE_2D_ARRAY:
      case eGL_TEXTURE_CUBE_MAP_ARRAY:
      case eGL_TEXTURE_2D_MULTISAMPLE_ARRAY:
        GL.glFramebufferTextureLayer(eGL_FRAMEBUFFER, attachment, readtex, level, 0);
        break;

      case eGL_TEXTURE_CUBE_MAP:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_X:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_X:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Z:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
      case eGL_TEXTURE_2D:
      case eGL_TEXTURE_2D_MULTISAMPLE:
      default:
        GL.glFramebufferTexture2D(eGL_FRAMEBUFFER, attachment, target, readtex, level);
        break;
    }

    GLenum status = GL.glCheckFramebufferStatus(eGL_FRAMEBUFFER);

    readDirectly = (status == eGL_FRAMEBUFFER_COMPLETE);
  }

  GLenum readFormat = format;
  GLenum readType = type;

  ResourceFormat origFmt = MakeResourceFormat(target, origInternalFormat);

  bool disableSRGBCorrect = false;

  // do a blit to the nearest compatible expanded format if we can't read directly
  if(!readDirectly)
  {
    ResourceFormat remappedFmt = origFmt;

    // special case luminance/alpha
    if(readFormat == eGL_ALPHA || readFormat == eGL_LUMINANCE ||
       origFmt.type == ResourceFormatType::A8)
    {
      RDCASSERTEQUAL(origFmt.compType, CompType::UNorm);
      RDCASSERTEQUAL(origFmt.compCount, 1);
      RDCASSERTEQUAL(origFmt.compByteWidth, 1);
      remappedFmt.compType = CompType::UNorm;
      remappedFmt.compCount = 1;
      remappedFmt.type = ResourceFormatType::Regular;
      remappedFmt.compByteWidth = 1;

      // we can read directly after the remap, because it's still a 1 component unorm texture it's
      // just now in the right format
      readDirectly = true;
    }
    else if(readFormat == eGL_LUMINANCE_ALPHA)
    {
      RDCASSERTEQUAL(origFmt.compType, CompType::UNorm);
      RDCASSERTEQUAL(origFmt.compCount, 2);
      RDCASSERTEQUAL(origFmt.compByteWidth, 1);
      remappedFmt.compType = CompType::UNorm;
      remappedFmt.compCount = 2;
      remappedFmt.type = ResourceFormatType::Regular;
      remappedFmt.compByteWidth = 1;

      readDirectly = true;
    }
    else
    {
      if(depthFormat)
      {
        // all depth formats we read back as RGBA float
        remappedFmt.compType = CompType::Float;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;

        // try full floats
        remappedFmt.compByteWidth = 4;

        // unless it's not supported
        if(!HasExt[OES_texture_float] && GLCoreVersion < 30)
        {
          remappedFmt.compByteWidth = 2;
          RDCDEBUG("Implementation doesn't support float color targets, reading as half-float");
        }
      }
      // for most regular formats we just try to remap to the 4-component version assuming that if
      // the smaller version is supported then the larger version is supported and FBO'able. This
      // should hold for RGB formats at least.
      else if(origFmt.type == ResourceFormatType::Regular &&
              (origFmt.compType == CompType::Float || origFmt.compType == CompType::UNorm ||
               origFmt.compType == CompType::UInt || origFmt.compType == CompType::SInt ||
               origFmt.compType == CompType::UNormSRGB))
      {
        remappedFmt.compCount = 4;
        remappedFmt.SetBGRAOrder(false);
      }
      // for SNorm formats remap to RGBA16F as well and hope it's supported. This loses precision on
      // RGBA16_SNORM but we accept that.
      else if(origFmt.compType == CompType::SNorm)
      {
        remappedFmt.compType = CompType::Float;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;
        remappedFmt.compByteWidth = 2;
      }
      // if it's a sub-1-byte unorm format, remap to RGBA8
      else if(origFmt.type == ResourceFormatType::R4G4 ||
              origFmt.type == ResourceFormatType::R4G4B4A4 ||
              origFmt.type == ResourceFormatType::R5G5B5A1 ||
              origFmt.type == ResourceFormatType::R5G6B5)
      {
        remappedFmt.compType = CompType::UNorm;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;
        remappedFmt.compByteWidth = 1;
        remappedFmt.SetBGRAOrder(false);
      }
      // similar with sub-16F special formats
      else if(origFmt.type == ResourceFormatType::R10G10B10A2 && origFmt.compType == CompType::UNorm)
      {
        remappedFmt.compType = CompType::UNorm;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;
        remappedFmt.compByteWidth = 1;
      }
      else if(origFmt.type == ResourceFormatType::R10G10B10A2 && origFmt.compType == CompType::UInt)
      {
        remappedFmt.compType = CompType::UInt;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;
        remappedFmt.compByteWidth = 1;
      }
      else if(origFmt.type == ResourceFormatType::R11G11B10 ||
              origFmt.type == ResourceFormatType::R9G9B9E5)
      {
        remappedFmt.compType = CompType::Float;
        remappedFmt.compCount = 4;
        remappedFmt.type = ResourceFormatType::Regular;
        remappedFmt.compByteWidth = 2;
      }
    }

    GLenum internalformat = MakeGLFormat(remappedFmt);
    GLenum remapformat = GetBaseFormat(internalformat);
    GLenum remaptype = GetDataType(internalformat);

    RDCDEBUG("Doing manual blit from %s to %s with format %s and type %s to allow readback",
             ToStr(origInternalFormat).c_str(), ToStr(internalformat).c_str(),
             ToStr(format).c_str(), ToStr(type).c_str());

    GLint baseLevel = 0;
    GLint maxLevel = 0;

    // for cubemaps we read them back face by face so we blit to a 2D texture, but we still need to
    // bind the source texture as a cubemap
    GLenum origTarget = target;
    GLenum bindTarget = target;
    switch(target)
    {
      case eGL_TEXTURE_CUBE_MAP:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_X:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_X:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Z:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
        target = eGL_TEXTURE_2D;
        bindTarget = eGL_TEXTURE_CUBE_MAP;
        break;
      default: break;
    }

    // sample from only the right level of the source texture
    GL.glGetTexParameteriv(bindTarget, eGL_TEXTURE_BASE_LEVEL, &baseLevel);
    GL.glGetTexParameteriv(bindTarget, eGL_TEXTURE_MAX_LEVEL, &maxLevel);
    GL.glTexParameteri(bindTarget, eGL_TEXTURE_BASE_LEVEL, level);
    GL.glTexParameteri(bindTarget, eGL_TEXTURE_MAX_LEVEL, level);

    // support 2D/Array/3D textures for now
    RDCASSERT((target == eGL_TEXTURE_2D) || (target == eGL_TEXTURE_2D_ARRAY) ||
                  (target == eGL_TEXTURE_3D),
              target);
    GL.glGenTextures(1, &readtex);
    GL.glBindTexture(target, readtex);

    // allocate the texture
    GL.glTexParameteri(target, eGL_TEXTURE_MAX_LEVEL, 0);
    if(target == eGL_TEXTURE_2D)
    {
      GL.glTexImage2D(target, 0, internalformat, width, height, 0, remapformat, remaptype, NULL);
      GL.glFramebufferTexture2D(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0, target, readtex, 0);
    }
    else
    {
      GL.glTexImage3D(target, 0, internalformat, width, height, depth, 0, remapformat, remaptype,
                      NULL);
      GL.glFramebufferTextureLayer(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0, readtex, 0, 0);
    }

    GLenum fbostatus = GL.glCheckFramebufferStatus(eGL_FRAMEBUFFER);

    if(fbostatus != eGL_FRAMEBUFFER_COMPLETE)
      RDCERR("glReadPixels emulation blit FBO is %s with format %s", ToStr(fbostatus).c_str(),
             ToStr(internalformat).c_str());

    // push rendering state
    GLPushPopState textState;

    textState.Push(true);

    // render a blit triangle to move alpha in the source to red in the dest
    GL.glDisable(eGL_BLEND);
    GL.glDisable(eGL_DEPTH_TEST);
    GL.glDisable(eGL_STENCIL_TEST);
    GL.glDisable(eGL_CULL_FACE);
    GL.glDisable(eGL_SCISSOR_TEST);
    GL.glViewport(0, 0, width, height);

    bool oldFBOsrgb = false;
    if(HasExt[EXT_framebuffer_sRGB])
    {
      oldFBOsrgb = GL.glIsEnabled(eGL_FRAMEBUFFER_SRGB) == GL_TRUE;
      GL.glEnable(eGL_FRAMEBUFFER_SRGB);
    }
    else if(origFmt.compType == CompType::UNormSRGB)
    {
      disableSRGBCorrect = true;
    }

    GL.glActiveTexture(eGL_TEXTURE0);
    GL.glBindTexture(bindTarget, boundTexture);

    GLuint prog;

    {
      const char *swizzle = "rgba";
      if(format == eGL_ALPHA)
      {
        swizzle = "aaaa";
      }
      else if(format == eGL_LUMINANCE || depthFormat)
      {
        swizzle = "rrrr";
      }
      else if(format == eGL_LUMINANCE_ALPHA)
      {
        swizzle = "raaa";
      }

      rdcstr vssource;
      rdcstr fssource;
      if(bindTarget == eGL_TEXTURE_CUBE_MAP)
      {
        vssource =
            "attribute vec2 pos;\n"
            "void main() { gl_Position = vec4(pos, 0.5, 0.5); }";

        rdcstr cubecoord = "\nvec3 CalcCubeCoord(vec2 uv) {\nuv -= vec2(0.5);\nvec3 coord;\n";

        if(origTarget == eGL_TEXTURE_CUBE_MAP_POSITIVE_X)
          cubecoord += "coord = vec3(0.5, -uv.y, -uv.x);\n";
        else if(origTarget == eGL_TEXTURE_CUBE_MAP_NEGATIVE_X)
          cubecoord += "coord = vec3(-0.5, -uv.y, uv.x);\n";
        else if(origTarget == eGL_TEXTURE_CUBE_MAP_POSITIVE_Y)
          cubecoord += "coord = vec3(uv.x, 0.5, uv.y);\n";
        else if(origTarget == eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y)
          cubecoord += "coord = vec3(uv.x, -0.5, -uv.y);\n";
        else if(origTarget == eGL_TEXTURE_CUBE_MAP_POSITIVE_Z)
          cubecoord += "coord = vec3(uv.x, -uv.y, 0.5);\n";
        else    // origTarget == eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z
          cubecoord += "coord = vec3(-uv.x, -uv.y, -0.5);\n";

        cubecoord += "\nreturn coord;\n}\n";

        fssource = rdcstr(
                       "precision highp float;\n"
                       "uniform vec3 res;\n"
                       "uniform samplerCube srcTex;\n") +
                   cubecoord +
                   "void main() { gl_FragColor = textureCube(srcTex, "
                   "CalcCubeCoord(vec2(gl_FragCoord.xy)/res.xy))." +
                   swizzle + "; }";
      }
      else if(target == eGL_TEXTURE_2D)
      {
        vssource =
            "attribute vec2 pos;\n"
            "void main() { gl_Position = vec4(pos, 0.5, 0.5); }";

        fssource =
            rdcstr(
                "precision highp float;\n"
                "uniform vec3 res;\n"
                "uniform sampler2D srcTex;\n"
                "void main() { gl_FragColor = texture2D(srcTex, vec2(gl_FragCoord.xy)/res.xy).") +
            swizzle + "; }";
      }
      else
      {
        vssource =
            "#version 300 es\n"
            "in vec2 pos;\n"
            "void main() { gl_Position = vec4(pos, 0.5, 0.5); }";

        const char *sampler = (target == eGL_TEXTURE_2D_ARRAY) ? "sampler2DArray" : "sampler3D";

        fssource =
            rdcstr(
                "#version 300 es\n"
                "uniform highp vec3 res;\n"
                "uniform highp ") +
            sampler +
            " srcTex;\n"
            "out highp vec4 color;\n"
            "void main() { color = texture(srcTex, vec3(gl_FragCoord.xy/res.xy, res.z).xyz)." +
            swizzle + "; }";
      }

      const char *vs = vssource.c_str();
      const char *fs = fssource.c_str();

      GLuint vert = GL.glCreateShader(eGL_VERTEX_SHADER);
      GLuint frag = GL.glCreateShader(eGL_FRAGMENT_SHADER);

      GL.glShaderSource(vert, 1, &vs, NULL);
      GL.glShaderSource(frag, 1, &fs, NULL);

      GL.glCompileShader(vert);
      GL.glCompileShader(frag);

      char buffer[1024] = {0};
      GLint status = 0;

      GL.glGetShaderiv(vert, eGL_COMPILE_STATUS, &status);
      if(status == 0)
      {
        GL.glGetShaderInfoLog(vert, 1024, NULL, buffer);
        RDCERR("Shader error: %s", buffer);
      }

      GL.glGetShaderiv(frag, eGL_COMPILE_STATUS, &status);
      if(status == 0)
      {
        GL.glGetShaderInfoLog(frag, 1024, NULL, buffer);
        RDCERR("Shader error: %s", buffer);
      }

      prog = GL.glCreateProgram();

      GL.glAttachShader(prog, vert);
      GL.glAttachShader(prog, frag);

      GL.glLinkProgram(prog);

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

      GL.glDeleteShader(vert);
      GL.glDeleteShader(frag);
    }

    // fullscreen triangle
    float verts[] = {
        -1.0f, -1.0f,    // vertex 0
        3.0f,  -1.0f,    // vertex 1
        -1.0f, 3.0f,     // vertex 2
    };

    GLuint vb = 0;
    GL.glGenBuffers(1, &vb);
    GL.glBindBuffer(eGL_ARRAY_BUFFER, vb);
    GL.glBufferData(eGL_ARRAY_BUFFER, sizeof(verts), verts, eGL_STATIC_DRAW);

    GLuint vao;
    GL.glGenVertexArrays(1, &vao);

    GL.glBindVertexArray(vao);

    GLint loc = GL.glGetAttribLocation(prog, "pos");
    GL.glVertexAttribPointer((GLuint)loc, 2, eGL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
    GL.glEnableVertexAttribArray((GLuint)loc);

    GL.glUseProgram(prog);

    GLenum depthMode = eGL_DEPTH_COMPONENT;
    if(depthFormat && HasExt[ARB_stencil_texturing])
    {
      GL.glGetTexParameteriv(bindTarget, eGL_DEPTH_STENCIL_TEXTURE_MODE, (GLint *)&depthMode);
    }

    for(int32_t d = 0; d < depth; ++d)
    {
      GL.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

      if(depthFormat && HasExt[ARB_stencil_texturing])
      {
        GL.glTexParameteri(bindTarget, eGL_DEPTH_STENCIL_TEXTURE_MODE, eGL_DEPTH_COMPONENT);
      }
      if(target != eGL_TEXTURE_2D)
      {
        GL.glFramebufferTextureLayer(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0, readtex, 0, d);
      }

      loc = GL.glGetUniformLocation(prog, "srcTex");
      GL.glUniform1i(loc, 0);
      loc = GL.glGetUniformLocation(prog, "res");
      GL.glUniform3f(loc, float(width), float(height), float(d));

      GL.glDrawArrays(eGL_TRIANGLES, 0, 3);

      // if we support reading stencil, read the stencil into green
      if(remapformat == eGL_DEPTH_STENCIL && HasExt[ARB_stencil_texturing])
      {
        GL.glTexParameteri(bindTarget, eGL_DEPTH_STENCIL_TEXTURE_MODE, eGL_STENCIL_INDEX);
        GL.glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_FALSE);
        GL.glDrawArrays(eGL_TRIANGLES, 0, 3);
      }
    }

    GL.glDeleteVertexArrays(1, &vao);
    GL.glDeleteBuffers(1, &vb);
    GL.glDeleteProgram(prog);

    if(HasExt[EXT_framebuffer_sRGB] && !oldFBOsrgb)
      GL.glDisable(eGL_FRAMEBUFFER_SRGB);

    // pop rendering state
    textState.Pop(true);

    // delete this texture once we're done, at the end of the function
    deltex = readtex;

    // restore base/max level as we changed them to sample from the right level above
    GL.glBindTexture(bindTarget, boundTexture);
    GL.glTexParameteri(bindTarget, eGL_TEXTURE_BASE_LEVEL, baseLevel);
    GL.glTexParameteri(bindTarget, eGL_TEXTURE_MAX_LEVEL, maxLevel);

    if(depthFormat && HasExt[ARB_stencil_texturing])
    {
      GL.glTexParameteri(bindTarget, eGL_DEPTH_STENCIL_TEXTURE_MODE, depthMode);
    }

    // read from the blitted texture from level 0, as red
    GL.glBindTexture(target, readtex);
    level = 0;
    readFormat = remapformat;
    readType = remaptype;

    attachment = eGL_COLOR_ATTACHMENT0;

    RDCDEBUG("Done blit");
  }

  size_t dstSliceSize = GetByteSize(width, height, 1, format, type);

  bool swizzleBGRA = false;

  // if we can't read BGRA natively, read as RGBA and swizzle manually
  if(!HasExt[EXT_read_format_bgra] && readFormat == eGL_BGRA)
  {
    readFormat = eGL_RGBA;
    swizzleBGRA = true;
  }

  if(!readDirectly && readFormat == eGL_RGBA && readType == eGL_UNSIGNED_SHORT_4_4_4_4)
  {
    readType = eGL_UNSIGNED_BYTE;
    swizzleBGRA = true;
  }

  for(GLint d = 0; d < depth; ++d)
  {
    switch(target)
    {
      case eGL_TEXTURE_3D:
      case eGL_TEXTURE_2D_ARRAY:
      case eGL_TEXTURE_CUBE_MAP_ARRAY:
      case eGL_TEXTURE_2D_MULTISAMPLE_ARRAY:
        GL.glFramebufferTextureLayer(eGL_FRAMEBUFFER, attachment, readtex, level, d);
        break;

      case eGL_TEXTURE_CUBE_MAP:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_X:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_X:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
      case eGL_TEXTURE_CUBE_MAP_POSITIVE_Z:
      case eGL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
      case eGL_TEXTURE_2D:
      case eGL_TEXTURE_2D_MULTISAMPLE:
      default:
        GL.glFramebufferTexture2D(eGL_FRAMEBUFFER, attachment, target, readtex, level);
        break;
    }

    GLenum status = GL.glCheckFramebufferStatus(eGL_FRAMEBUFFER);

    if(status != eGL_FRAMEBUFFER_COMPLETE)
      RDCERR("glReadPixels emulation FBO is %s", ToStr(status).c_str());

    byte *dst = (byte *)pixels + d * dstSliceSize;

    // pick the read format/type

    if((readFormat == eGL_RGBA && readType == eGL_UNSIGNED_BYTE) ||
       (readFormat == eGL_RGBA_INTEGER && readType == eGL_UNSIGNED_INT) ||
       (readFormat == eGL_RGBA_INTEGER && readType == eGL_INT) ||
       (readFormat == eGL_RGBA && readType == eGL_UNSIGNED_INT_2_10_10_10_REV))
    {
      // if we were already planning to use one of the spec-guaranteed supported combinations,
      // great! use that
    }
    else
    {
      // see what format and type the implementation supports
      GLenum implFormat = eGL_NONE, implType = eGL_NONE;

      if(!depthFormat)
      {
        GL.glGetIntegerv(eGL_IMPLEMENTATION_COLOR_READ_FORMAT, (GLint *)&implFormat);
        GL.glGetIntegerv(eGL_IMPLEMENTATION_COLOR_READ_TYPE, (GLint *)&implType);

        // GL_HALF_FLOAT and GL_HALF_FLOAT_OES have different enum values but are the same
        // otherwise. we always use the normal enum ourselves, but if the driver wants the _OES
        // version then just use that so we match and can do a direct readback.
        if(implType == eGL_HALF_FLOAT_OES && readType == eGL_HALF_FLOAT)
          readType = eGL_HALF_FLOAT_OES;
      }

      if(!depthFormat && implFormat == readFormat && implType == readType)
      {
        // great, the implementation supports the format and type we want
      }
      else
      {
        // need to remap now from what we read to what we need to write.
        readDirectly = false;

        // if all that's different is the number of components, read as one of the
        // guaranteed-supported format/type pairs
        if((readFormat == eGL_RGBA || readFormat == eGL_RGB || readFormat == eGL_RG ||
            readFormat == eGL_RED) &&
           (readType == eGL_UNSIGNED_BYTE || readType == eGL_UNSIGNED_SHORT_4_4_4_4))
        {
          readFormat = eGL_RGBA;
          readType = eGL_UNSIGNED_BYTE;
        }
        else if((readFormat == eGL_RGBA_INTEGER || readFormat == eGL_RGB_INTEGER ||
                 readFormat == eGL_RG_INTEGER || readFormat == eGL_RED_INTEGER) &&
                (readType == eGL_UNSIGNED_BYTE || readType == eGL_UNSIGNED_SHORT ||
                 readType == eGL_UNSIGNED_INT))
        {
          readFormat = eGL_RGBA_INTEGER;
          readType = eGL_UNSIGNED_INT;
        }
        else if((readFormat == eGL_RGBA_INTEGER || readFormat == eGL_RGB_INTEGER ||
                 readFormat == eGL_RG_INTEGER || readFormat == eGL_RED_INTEGER) &&
                (readType == eGL_BYTE || readType == eGL_SHORT || readType == eGL_INT))
        {
          readFormat = eGL_RGBA_INTEGER;
          readType = eGL_INT;
        }
        else
        {
          // TODO maybe fallback to one of the guaranteed ones? or find a way to negotiate a better
          // one?
          RDCWARN(
              "Implementation reported format %s / type %s readback format for %s. Trying with "
              "desired format %s / type %s pair anyway.",
              ToStr(implFormat).c_str(), ToStr(implType).c_str(), ToStr(origInternalFormat).c_str(),
              ToStr(readFormat).c_str(), ToStr(readType).c_str());
        }
      }
    }

    PixelUnpackState unpack;
    PixelPackState pack;
    unpack.Fetch(false);
    pack.Fetch(false);

    ResetPixelPackState(false, 1);
    ResetPixelUnpackState(false, 1);

    if(readDirectly)
    {
      // fast path, we're reading directly in the exact format
      memset(dst, 0, dstSliceSize);
      GL.glReadPixels(0, 0, width, height, readFormat, readType, (void *)dst);
    }
    else
    {
      RDCDEBUG("Readback with remapping for texture format %s", ToStr(origInternalFormat).c_str());

      // unfortunately the readback is not in the right format directly, we'll need to read back
      // whatever we got and then convert to the output format.

      size_t sliceSize = GetByteSize(width, height, 1, readFormat, readType);
      byte *readback = new byte[sliceSize];
      GL.glReadPixels(0, 0, width, height, readFormat, readType, readback);

      uint32_t readCompCount = 1;
      if(readFormat == eGL_RGBA || readFormat == eGL_RGBA_INTEGER)
        readCompCount = 4;
      else if(readFormat == eGL_RGB || readFormat == eGL_RGB_INTEGER)
        readCompCount = 3;
      else if(readFormat == eGL_RG || readFormat == eGL_RG_INTEGER)
        readCompCount = 2;
      else if(readFormat == eGL_RED || readFormat == eGL_RED_INTEGER)
        readCompCount = 1;
      else if(readFormat == eGL_DEPTH_COMPONENT || readFormat == eGL_STENCIL_INDEX)
        readCompCount = 1;
      else if(readFormat == eGL_DEPTH_STENCIL)
        readCompCount = 2;
      else
        RDCERR("Unexpected implementation format %s, assuming one component",
               ToStr(readFormat).c_str());

      // how big is a component (1/2/4 bytes)
      size_t readCompSize = GetByteSize(1, 1, 1, eGL_RED, readType);

      if(depthFormat)
        readCompSize = 4;

      // if the type didn't change from what the caller expects, we only changed the number of
      // components. This is easy to remap
      if(type == readType && !disableSRGBCorrect && !depthFormat &&
         (origFmt.type == ResourceFormatType::Regular || origFmt.type == ResourceFormatType::A8))
      {
        RDCDEBUG("Component number changed only");

        uint32_t dstCompCount = origFmt.compCount;

        byte *srcPixel = readback;
        byte *dstPixel = dst;

        // for each pixel
        for(GLint i = 0; i < width * height; i++)
        {
          // copy RGB
          memcpy(dstPixel, srcPixel, readCompSize * dstCompCount);

          // advance dst by RGB
          dstPixel += readCompSize * dstCompCount;

          // advance src by RGBA
          srcPixel += readCompSize * readCompCount;
        }
      }
      else
      {
        RDCDEBUG("Component format changed");

        ResourceFormat readFmt;
        readFmt.type = ResourceFormatType::Regular;
        readFmt.compCount = readCompCount & 0xff;
        readFmt.compByteWidth = readCompSize & 0xff;

        if(IsSIntFormat(GetSizedFormat(readFormat)))
        {
          switch(readType)
          {
            case eGL_UNSIGNED_INT:
            case eGL_UNSIGNED_SHORT:
            case eGL_UNSIGNED_BYTE: readFmt.compType = CompType::UInt; break;
            case eGL_INT:
            case eGL_SHORT:
            case eGL_BYTE: readFmt.compType = CompType::SInt; break;
            default:
              RDCERR("Unexpected readType %s", ToStr(readType).c_str());
              readFmt.compType = CompType::UInt;
              break;
          }
        }
        else
        {
          switch(readType)
          {
            case eGL_UNSIGNED_INT:
            case eGL_UNSIGNED_SHORT:
            case eGL_UNSIGNED_BYTE: readFmt.compType = CompType::UNorm; break;
            case eGL_INT:
            case eGL_SHORT:
            case eGL_BYTE: readFmt.compType = CompType::SNorm; break;
            case eGL_HALF_FLOAT_OES:
            case eGL_HALF_FLOAT:
            case eGL_FLOAT:
            case eGL_DOUBLE: readFmt.compType = CompType::Float; break;
            default:
              RDCERR("Unexpected readType %s", ToStr(readType).c_str());
              readFmt.compType = CompType::UNorm;
              break;
          }

          // if we couldn't enable FRAMEBUFFER_SRGB to ensure the blit is srgb-preserving, we wrote
          // out linear data. So we need to under-correct to at least get values approximately right
          // even if we lost some information by missing a correct. Since we can't control whether a
          // sRGB texture is read as sRGB.
          if(!disableSRGBCorrect && origFmt.compType == CompType::UNormSRGB)
            readFmt.compType = CompType::UNormSRGB;
        }

        byte *srcPixel = readback;
        byte *dstPixel = dst;

        size_t dstStride = origFmt.ElementSize();

        bool d24 = false;
        // D24 is not written tightly packed, add extra byte for padding
        if(origFmt.type == ResourceFormatType::Regular && origFmt.compCount == 1 &&
           origFmt.compByteWidth == 3 && origFmt.compType == CompType::Depth)
        {
          d24 = true;
          dstStride = 4;
          RDCDEBUG("Handling D24 only");
        }

        // go pixel-by-pixel, reading in the readback format and writing in the dest format
        for(GLint i = 0; i < width * height; i++)
        {
          FloatVector vec = DecodeFormattedComponents(readFmt, srcPixel);

          EncodeFormattedComponents(origFmt, vec, dstPixel);

          // GL expects ABGR order for these formats where our standard encoder writes BGRA, swizzle
          // here
          if(origFmt.type == ResourceFormatType::R4G4B4A4)
          {
            uint16_t val = 0;
            memcpy(&val, dstPixel, sizeof(val));
            val = ((val & 0x0fff) << 4) | ((val & 0xf000) >> 12);
            memcpy(dstPixel, &val, sizeof(val));
          }
          else if(origFmt.type == ResourceFormatType::R5G5B5A1)
          {
            uint16_t val = 0;
            memcpy(&val, dstPixel, sizeof(val));
            val = ((val & 0x7fff) << 1) | ((val & 0x8000) >> 12);
            memcpy(dstPixel, &val, sizeof(val));
          }

          if(d24)
          {
            // normally we'd expect D24 to be in the bottom 3 bytes, since D24S8 puts stencil in the
            // top byte. However on upload GL doesn't really support a proper preserving upload (or
            // not portably) so we specify UNSIGNED_INT. Shifting like this is what a proper GL
            // implementation does on read and gives us the right results.
            uint32_t *p = (uint32_t *)dstPixel;
            *p = (*p << 8) | (*p >> 16);
          }

          dstPixel += dstStride;
          srcPixel += readCompSize * readCompCount;
        }
      }

      delete[] readback;
    }

    unpack.Apply(false);
    pack.Apply(false);

    if(swizzleBGRA)
    {
      // since we read back the texture with RGBA format, we have to flip the R and B components
      byte *b = dst;
      for(GLint i = 0, n = width * height; i < n; ++i, b += 4)
        std::swap(b[0], b[2]);
    }
  }

  if(deltex)
  {
    GL.glDeleteTextures(1, &deltex);
    GL.glBindTexture(target, boundTexture);
  }

  GL.glDeleteFramebuffers(1, &fbo);
}