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

#include <algorithm>
#include "data/glsl_shaders.h"
#include "maths/formatpacking.h"
#include "gl_common.h"
#include "gl_driver.h"
#include "gl_replay.h"

namespace
{
bool isDirectWrite(ResourceUsage usage)
{
  return ((usage >= ResourceUsage::VS_RWResource && usage <= ResourceUsage::CS_RWResource) ||
          usage == ResourceUsage::CopyDst || usage == ResourceUsage::Copy ||
          usage == ResourceUsage::Resolve || usage == ResourceUsage::ResolveDst ||
          usage == ResourceUsage::GenMips);
}

enum class ModType
{
  PreMod,
  PostMod,
};

struct FramebufferKey
{
  ModType modType;
  GLenum colorFormat;
  GLenum depthFormat;
  GLenum stencilFormat;
  uint32_t numSamples;
  bool operator<(const FramebufferKey &other) const
  {
    if(modType != other.modType)
      return modType < other.modType;
    if(colorFormat != other.colorFormat)
      return colorFormat < other.colorFormat;
    if(depthFormat != other.depthFormat)
      return depthFormat < other.depthFormat;
    if(stencilFormat != other.stencilFormat)
      return stencilFormat < other.stencilFormat;
    return numSamples < other.numSamples;
  }
};

struct CopyFramebuffer
{
  GLuint framebufferId;
  GLuint colorTextureId;
  GLuint dsTextureId;
  GLuint depthTextureId;
  GLuint stencilTextureId;
  GLuint stencilViewId;
  uint32_t width;
  FramebufferKey format;
};

struct ShaderOutFramebuffer
{
  GLuint framebufferId;
  GLuint colorTextureId;
  GLuint dsTextureId;
  FramebufferKey format;
};

struct GLPixelHistoryResources
{
  ResourceId target;
  bool depthTarget;

  // a framebuffer with the target resource bound, used for copying for pre/post mod on direct writes
  // (compute dispatches or clears) where the currently bound framebuffer won't necessarily match
  GLuint copySourceFramebuffer;

  // Used for offscreen rendering for draw call events.
  GLuint fullPrecisionColorImage;
  GLuint fullPrecisionDsImage;
  GLuint fullPrecisionFrameBuffer;
  GLuint primitiveIdFragmentShader;
  GLuint primitiveIdFragmentShaderSPIRV;
  GLuint fixedColFragmentShader;
  GLuint fixedColFragmentShaderSPIRV;
  GLuint msCopyComputeProgram;
  GLuint msCopyDepthComputeProgram;
  GLuint msCopyDstBuffer;
  GLuint msCopyUniformBlockBuffer;
  std::unordered_map<GLuint, GLuint> primIdPrograms;
  std::unordered_map<GLuint, GLuint> fixedColPrograms;
  std::map<FramebufferKey, CopyFramebuffer> copyFramebuffers;
  std::map<FramebufferKey, ShaderOutFramebuffer> shaderOutIntFramebuffers;
};

enum class OpenGLTest
{
  FaceCulling,
  ScissorTest,
  DepthClamp,
  DepthBounds,
  Discard,
  StencilTest,
  DepthTest,
  SampleMask,
  NumTests
};

enum class PerFragmentQueryType
{
  ShaderOut,
  PostMod,
  PrimitiveId
};

GLuint RefreshUniforms(WrappedOpenGL *driver, GLRenderState &rs, GLuint dstProgram)
{
  ContextPair &ctx = driver->GetCtx();

  GLuint srcProgram = 0;

  if(rs.Program.name)
  {
    srcProgram = rs.Program.name;
  }
  else if(rs.Pipeline.name)
  {
    ResourceId id = driver->GetResourceManager()->GetResID(ProgramPipeRes(ctx, rs.Pipeline.name));
    const WrappedOpenGL::PipelineData &pipeDetails = driver->GetPipeline(id);

    srcProgram = driver->GetResourceManager()
                     ->GetResource(pipeDetails.stagePrograms[(size_t)ShaderStage::Pixel])
                     .name;
  }

  PerStageReflections stages;
  driver->FillReflectionArray(ProgramRes(ctx, srcProgram), stages);

  PerStageReflections dstStages;
  driver->FillReflectionArray(ProgramRes(ctx, dstProgram), dstStages);

  CopyProgramUniforms(stages, srcProgram, dstStages, dstProgram);

  return dstProgram;
}

GLuint GetFixedColProgram(WrappedOpenGL *driver, GLReplay *replay,
                          GLPixelHistoryResources &resources, GLuint currentProgram)
{
  GLRenderState rs;
  rs.FetchState(driver);

  auto programIterator = resources.fixedColPrograms.find(currentProgram);
  if(programIterator != resources.fixedColPrograms.end())
  {
    return RefreshUniforms(driver, rs, programIterator->second);
  }

  GLuint ret = driver->glCreateProgram();
  replay->CreateShaderReplacementProgram(rs.Program.name, rs.Pipeline.name, ret, ShaderStage::Pixel,
                                         resources.fixedColFragmentShader,
                                         resources.fixedColFragmentShaderSPIRV);

  resources.fixedColPrograms[currentProgram] = ret;

  return ret;
}

GLuint GetPrimitiveIdProgram(WrappedOpenGL *driver, GLReplay *replay,
                             GLPixelHistoryResources &resources, GLuint currentProgram)
{
  GLRenderState rs;
  rs.FetchState(driver);

  auto programIterator = resources.primIdPrograms.find(currentProgram);
  if(programIterator != resources.primIdPrograms.end())
  {
    return RefreshUniforms(driver, rs, programIterator->second);
  }

  GLuint ret = driver->glCreateProgram();
  replay->CreateShaderReplacementProgram(rs.Program.name, rs.Pipeline.name, ret, ShaderStage::Pixel,
                                         resources.primitiveIdFragmentShader,
                                         resources.primitiveIdFragmentShaderSPIRV);

  resources.primIdPrograms[currentProgram] = ret;

  return ret;
}

// Returns a Framebuffer that has the same depth and stencil formats of the currently bound
// framebuffer
// so that you can blit from the current bound framebuffer into the new framebuffer
const CopyFramebuffer &getCopyFramebuffer(WrappedOpenGL *driver,
                                          std::map<FramebufferKey, CopyFramebuffer> &copyFramebuffers,
                                          ModType modType, uint32_t numSamples, uint32_t numEvents,
                                          GLenum depthFormat, GLenum stencilFormat,
                                          GLenum colorFormat)
{
  bool multisampled = numSamples > 1;

  GLuint curDepth;
  GLint depthType;
  GLuint curStencil;
  GLint stencilType;

  if(colorFormat == eGL_NONE)
    colorFormat = eGL_RGBA8;

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curDepth);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &depthType);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curStencil);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &stencilType);

  FramebufferKey key = {modType, colorFormat, depthFormat, stencilFormat, numSamples};
  auto it = copyFramebuffers.find(key);
  if(it != copyFramebuffers.end())
  {
    return it->second;
  }

  GLint savedReadFramebuffer, savedDrawFramebuffer;
  driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);
  driver->glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, &savedReadFramebuffer);

  CopyFramebuffer copyFramebuffer;
  RDCEraseEl(copyFramebuffer);
  copyFramebuffer.width = RDCMAX(100U, numEvents);
  // Allocate a framebuffer that will render to the textures
  driver->glGenFramebuffers(1, &copyFramebuffer.framebufferId);
  driver->glBindFramebuffer(eGL_FRAMEBUFFER, copyFramebuffer.framebufferId);

  // Allocate a texture for the pixel history colour values
  GLenum textureTarget = multisampled ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
  driver->glGenTextures(1, &copyFramebuffer.colorTextureId);
  driver->CreateTextureImage(copyFramebuffer.colorTextureId, colorFormat, eGL_NONE, eGL_NONE,
                             textureTarget, 2, copyFramebuffer.width, 1, 1, numSamples, 1);
  driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0,
                               copyFramebuffer.colorTextureId, 0);

  // Allocate a texture(s) for the pixel history depth/stencil values matching the capture's
  // frambebuffer's formats
  if(curStencil == curDepth && curStencil != 0)
  {
    driver->glGenTextures(1, &copyFramebuffer.dsTextureId);
    driver->CreateTextureImage(copyFramebuffer.dsTextureId, depthFormat, eGL_NONE, eGL_NONE,
                               textureTarget, 2, copyFramebuffer.width, 1, 1, numSamples, 1);
    driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_DEPTH_STENCIL_ATTACHMENT,
                                 copyFramebuffer.dsTextureId, 0);

    if(multisampled)
    {
      driver->glGenTextures(1, &copyFramebuffer.stencilViewId);
      driver->glTextureView(copyFramebuffer.stencilViewId, textureTarget,
                            copyFramebuffer.dsTextureId, depthFormat, 0, 1, 0, 1);
    }
  }
  else
  {
    if(curDepth != 0)
    {
      driver->glGenTextures(1, &copyFramebuffer.depthTextureId);
      driver->CreateTextureImage(copyFramebuffer.depthTextureId, depthFormat, eGL_NONE, eGL_NONE,
                                 textureTarget, 2, copyFramebuffer.width, 1, 1, numSamples, 1);
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                   copyFramebuffer.depthTextureId, 0);
    }
    if(curStencil != 0)
    {
      driver->glGenTextures(1, &copyFramebuffer.stencilTextureId);
      driver->CreateTextureImage(copyFramebuffer.stencilTextureId, stencilFormat, eGL_NONE, eGL_NONE,
                                 textureTarget, 2, copyFramebuffer.width, 1, 1, numSamples, 1);
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                   copyFramebuffer.stencilTextureId, 0);
    }
  }

  // restore the capture's framebuffer
  driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
  driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);

  copyFramebuffer.format = key;
  copyFramebuffers[key] = copyFramebuffer;
  return copyFramebuffers[key];
}

GLenum getTextureFormatType(GLenum internalFormat)
{
  if(IsUIntFormat(internalFormat))
  {
    return eGL_UNSIGNED_INT;
  }
  if(IsSIntFormat(internalFormat))
  {
    return eGL_INT;
  }
  // There are other format types that this format could belong to, but they can be blitted between
  // each other
  return eGL_UNSIGNED_NORMALIZED;
}

GLenum getCurrentTextureFormat(WrappedOpenGL *driver, uint32_t colIdx)
{
  GLuint curColor;
  GLint colorType;

  driver->glGetFramebufferAttachmentParameteriv(
      eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
      eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, (GLint *)&curColor);
  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER,
                                                GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &colorType);

  GLenum colorFormat = eGL_NONE;

  if(curColor != 0)
  {
    ResourceId id;
    if(colorType != eGL_RENDERBUFFER)
    {
      id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curColor));
    }
    else
    {
      id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curColor));
    }
    colorFormat = driver->m_Textures[id].internalFormat;
  }

  return colorFormat;
}

const CopyFramebuffer &getCopyFramebuffer(WrappedOpenGL *driver, uint32_t colIdx,
                                          std::map<FramebufferKey, CopyFramebuffer> &copyFramebuffers,
                                          ModType modType, uint32_t numSamples, uint32_t numEvents)
{
  GLuint curDepth;
  GLint depthType;
  GLuint curStencil;
  GLint stencilType;
  GLuint curColor;
  GLint colorType;

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curDepth);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &depthType);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curStencil);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &stencilType);

  driver->glGetFramebufferAttachmentParameteriv(
      eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
      eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, (GLint *)&curColor);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER,
                                                GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &colorType);

  GLenum depthFormat = eGL_NONE;

  if(curDepth != 0)
  {
    ResourceId id;
    if(depthType != eGL_RENDERBUFFER)
    {
      id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curDepth));
    }
    else
    {
      id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curDepth));
    }
    depthFormat = driver->m_Textures[id].internalFormat;
  }

  GLenum stencilFormat = eGL_NONE;
  if(curStencil != 0)
  {
    ResourceId id;
    if(stencilType != eGL_RENDERBUFFER)
    {
      id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curStencil));
    }
    else
    {
      id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curStencil));
    }
    stencilFormat = driver->m_Textures[id].internalFormat;
  }

  GLenum colorFormat = eGL_NONE;
  if(curColor != 0)
  {
    ResourceId id;
    if(colorType != eGL_RENDERBUFFER)
    {
      id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curColor));
    }
    else
    {
      id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curColor));
    }
    colorFormat = driver->m_Textures[id].internalFormat;
  }

  return getCopyFramebuffer(driver, copyFramebuffers, modType, numSamples, numEvents, depthFormat,
                            stencilFormat, colorFormat);
}

ShaderOutFramebuffer getShaderOutFramebuffer(WrappedOpenGL *driver, uint32_t colIdx,
                                             GLPixelHistoryResources &resources, FramebufferKey key,
                                             uint32_t width, uint32_t height)
{
  std::map<FramebufferKey, ShaderOutFramebuffer> &shaderOutFramebuffers =
      resources.shaderOutIntFramebuffers;
  auto it = shaderOutFramebuffers.find(key);

  if(it != shaderOutFramebuffers.end())
  {
    driver->glBindFramebuffer(eGL_FRAMEBUFFER, it->second.framebufferId);
    for(uint32_t i = 0; i < 8; i++)
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + i), 0, 0);
    driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                                 it->second.colorTextureId, 0);
    return it->second;
  }

  GLint savedReadFramebuffer, savedDrawFramebuffer;
  driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);
  driver->glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, &savedReadFramebuffer);

  bool multisampled = key.numSamples > 1;
  GLenum textureTarget = multisampled ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
  ShaderOutFramebuffer shaderOutFramebuffer;
  RDCEraseEl(shaderOutFramebuffer);

  // Allocate a framebuffer that will render to the textures
  driver->glGenFramebuffers(1, &shaderOutFramebuffer.framebufferId);
  driver->glBindFramebuffer(eGL_FRAMEBUFFER, shaderOutFramebuffer.framebufferId);

  // Allocate a texture for the pixel history colour values
  driver->glGenTextures(1, &shaderOutFramebuffer.colorTextureId);
  driver->glBindTexture(textureTarget, shaderOutFramebuffer.colorTextureId);
  driver->CreateTextureImage(shaderOutFramebuffer.colorTextureId, key.colorFormat, eGL_NONE,
                             eGL_NONE, textureTarget, 2, width, height, 1, key.numSamples, 1);
  driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                               shaderOutFramebuffer.colorTextureId, 0);

  // Allocate a texture for the pixel history depth/stencil values
  driver->glGenTextures(1, &shaderOutFramebuffer.dsTextureId);
  driver->glBindTexture(textureTarget, shaderOutFramebuffer.dsTextureId);
  driver->CreateTextureImage(shaderOutFramebuffer.dsTextureId, eGL_DEPTH32F_STENCIL8, eGL_NONE,
                             eGL_NONE, textureTarget, 2, width, height, 1, key.numSamples, 1);
  driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_DEPTH_STENCIL_ATTACHMENT,
                               shaderOutFramebuffer.dsTextureId, 0);

  // restore the capture's framebuffer
  driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
  driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);

  return (shaderOutFramebuffers[key] = shaderOutFramebuffer);
}

GLbitfield getFramebufferCopyMask(WrappedOpenGL *driver)
{
  GLuint curDepth = 0;
  GLuint curStencil = 0;
  GLuint curColor = 0;

  GLint colorAttachment;
  driver->glGetIntegerv(eGL_READ_BUFFER, &colorAttachment);

  if(colorAttachment)
  {
    driver->glGetFramebufferAttachmentParameteriv(eGL_READ_FRAMEBUFFER, GLenum(colorAttachment),
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                  (GLint *)&curColor);
  }
  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curDepth);

  driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                (GLint *)&curStencil);

  GLbitfield mask = 0;
  if(curColor)
  {
    mask |= eGL_COLOR_BUFFER_BIT;
  }
  if(curDepth)
  {
    mask |= eGL_DEPTH_BUFFER_BIT;
  }
  if(curStencil)
  {
    mask |= eGL_STENCIL_BUFFER_BIT;
  }
  return mask;
}

uint32_t getFramebufferColIndex(WrappedOpenGL *driver, ResourceId target)
{
  WrappedOpenGL &drv = *driver;
  GLResourceManager *rm = driver->GetResourceManager();

  GLuint numCols = 8;
  drv.glGetIntegerv(eGL_MAX_COLOR_ATTACHMENTS, (GLint *)&numCols);

  GLuint curCol[32] = {0};
  GLuint curDepth = 0;
  GLuint curStencil = 0;
  bool rbCol[32] = {false};
  bool rbDepth = false;
  bool rbStencil = false;

  GLenum type = eGL_TEXTURE;
  for(GLuint i = 0; i < numCols; i++)
  {
    drv.glGetFramebufferAttachmentParameteriv(
        eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + i),
        eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, (GLint *)&curCol[i]);
    drv.glGetFramebufferAttachmentParameteriv(
        eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + i),
        eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, (GLint *)&type);
    if(type == eGL_RENDERBUFFER)
      rbCol[i] = true;
  }

  drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                            eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                            (GLint *)&curDepth);
  drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                            eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, (GLint *)&type);
  if(type == eGL_RENDERBUFFER)
    rbDepth = true;
  drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                            eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                            (GLint *)&curStencil);
  drv.glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                            eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, (GLint *)&type);
  if(type == eGL_RENDERBUFFER)
    rbStencil = true;

  for(GLuint i = 0; i < numCols; i++)
  {
    ResourceId id = rm->GetResID(rbCol[i] ? RenderbufferRes(driver->GetCtx(), curCol[i])
                                          : TextureRes(driver->GetCtx(), curCol[i]));

    if(id == target)
      return i;
  }

  ResourceId depth = rm->GetResID(rbDepth ? RenderbufferRes(driver->GetCtx(), curDepth)
                                          : TextureRes(driver->GetCtx(), curDepth));
  ResourceId stencil = rm->GetResID(rbStencil ? RenderbufferRes(driver->GetCtx(), curStencil)
                                              : TextureRes(driver->GetCtx(), curStencil));

  // silently return attachment 0, it doesn't matter which colour attachment we use
  if(depth == target || stencil == target)
    return 0;

  RDCWARN("Couldn't find attachment %s anywhere in this framebuffer", ToStr(target).c_str());

  return 0;
}

bool PixelHistorySetupResources(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                                const TextureDescription &desc, const Subresource &sub,
                                uint32_t numEvents, GLuint glslVersion, uint32_t numSamples)
{
  driver->glGenFramebuffers(1, &resources.copySourceFramebuffer);
  driver->glBindFramebuffer(eGL_FRAMEBUFFER, resources.copySourceFramebuffer);

  GLResource targetTex = driver->GetResourceManager()->GetResource(desc.resourceId);
  GLenum targetAtt = eGL_COLOR_ATTACHMENT0;
  resources.depthTarget = false;
  if(desc.format.type == ResourceFormatType::D16S8 ||
     desc.format.type == ResourceFormatType::D24S8 || desc.format.type == ResourceFormatType::D32S8)
  {
    targetAtt = eGL_DEPTH_STENCIL_ATTACHMENT;
    resources.depthTarget = true;
  }
  else if(desc.format.compType == CompType::Depth)
  {
    targetAtt = eGL_DEPTH_ATTACHMENT;
    resources.depthTarget = true;
  }
  else if(desc.format.type == ResourceFormatType::S8)
  {
    targetAtt = eGL_STENCIL_ATTACHMENT;
    resources.depthTarget = true;
  }

  if(targetTex.Namespace == eResRenderbuffer)
  {
    driver->glFramebufferRenderbuffer(eGL_FRAMEBUFFER, targetAtt, eGL_RENDERBUFFER, targetTex.name);
  }
  else
  {
    if(desc.arraysize > 1)
      driver->glFramebufferTextureLayer(eGL_FRAMEBUFFER, targetAtt, targetTex.name, sub.mip,
                                        sub.slice);
    else
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, targetAtt, targetTex.name, sub.mip);
  }

  // Allocate a framebuffer that will render to the textures
  driver->glGenFramebuffers(1, &resources.fullPrecisionFrameBuffer);
  driver->glBindFramebuffer(eGL_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);

  // Allocate a texture for the pixel history colour values
  bool multisampled = numSamples > 1;
  GLenum textureTarget = multisampled ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
  driver->glGenTextures(1, &resources.fullPrecisionColorImage);
  driver->glBindTexture(textureTarget, resources.fullPrecisionColorImage);
  driver->CreateTextureImage(resources.fullPrecisionColorImage, eGL_RGBA32F, eGL_NONE, eGL_NONE,
                             textureTarget, 2, desc.width >> sub.mip, desc.height >> sub.mip, 1,
                             numSamples, 1);
  driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0,
                               resources.fullPrecisionColorImage, 0);

  // Allocate a texture for the pixel history depth/stencil values
  driver->glGenTextures(1, &resources.fullPrecisionDsImage);
  driver->glBindTexture(textureTarget, resources.fullPrecisionDsImage);
  driver->CreateTextureImage(resources.fullPrecisionDsImage, eGL_DEPTH32F_STENCIL8, eGL_NONE,
                             eGL_NONE, textureTarget, 2, desc.width >> sub.mip,
                             desc.height >> sub.mip, 1, numSamples, 1);
  driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_DEPTH_STENCIL_ATTACHMENT,
                               resources.fullPrecisionDsImage, 0);

  // We will blit the color values to this texture and then use a compute shader to get the values
  // for the sample that we want
  driver->glGenBuffers(1, &resources.msCopyDstBuffer);
  driver->glBindBuffer(eGL_SHADER_STORAGE_BUFFER, resources.msCopyDstBuffer);
  driver->glNamedBufferDataEXT(
      resources.msCopyDstBuffer,
      8 * (sizeof(float)) * numEvents,    // 8 floats per event (r,g,b,a,depth,null,null,null)
      NULL, eGL_DYNAMIC_READ);

  driver->glGenBuffers(1, &resources.msCopyUniformBlockBuffer);
  driver->glBindBuffer(eGL_UNIFORM_BUFFER, resources.msCopyUniformBlockBuffer);
  driver->glNamedBufferDataEXT(resources.msCopyUniformBlockBuffer, 8 * sizeof(uint32_t), NULL,
                               eGL_DYNAMIC_DRAW);

  // If the GLSL version is greater than or equal to 330, we can use IntBitsToFloat, otherwise we
  // need to write the float value directly.

  ShaderType shaderType = IsGLES ? ShaderType::GLSLES : ShaderType::GLSL;

  rdcstr primIdGLSL;
  if(glslVersion >= 330 || (IsGLES && glslVersion >= 300))
  {
    primIdGLSL = GenerateGLSLShader(GetEmbeddedResource(glsl_pixelhistory_primid_frag), shaderType,
                                    glslVersion);
  }
  else
  {
    primIdGLSL = GenerateGLSLShader(GetEmbeddedResource(glsl_pixelhistory_primid_frag), shaderType,
                                    glslVersion, "#define INT_BITS_TO_FLOAT_NOT_SUPPORTED\n");
  }

  // SPIR-V shaders are always generated as desktop GL 430, for ease
  rdcstr primIdSPIRV = GenerateGLSLShader(GetEmbeddedResource(glsl_pixelhistory_primid_frag),
                                          ShaderType::GLSPIRV, 430);

  rdcstr fixedColGLSL =
      GenerateGLSLShader(GetEmbeddedResource(glsl_fixedcol_frag), shaderType, glslVersion);
  rdcstr fixedColSPIRV =
      GenerateGLSLShader(GetEmbeddedResource(glsl_fixedcol_frag), ShaderType::GLSPIRV, 430);

  rdcstr msCopySource = GenerateGLSLShader(GetEmbeddedResource(glsl_pixelhistory_mscopy_comp),
                                           shaderType, glslVersion);
  rdcstr msCopySourceDepth = GenerateGLSLShader(
      GetEmbeddedResource(glsl_pixelhistory_mscopy_depth_comp), shaderType, glslVersion);

  if(HasExt[ARB_gl_spirv])
  {
    resources.primitiveIdFragmentShaderSPIRV = CreateSPIRVShader(eGL_FRAGMENT_SHADER, primIdSPIRV);
    resources.fixedColFragmentShaderSPIRV = CreateSPIRVShader(eGL_FRAGMENT_SHADER, fixedColSPIRV);
  }

  resources.primitiveIdFragmentShader = CreateShader(eGL_FRAGMENT_SHADER, primIdGLSL);
  resources.fixedColFragmentShader = CreateShader(eGL_FRAGMENT_SHADER, fixedColGLSL);
  resources.msCopyComputeProgram = CreateCShaderProgram(msCopySource);
  resources.msCopyDepthComputeProgram = CreateCShaderProgram(msCopySourceDepth);

  return true;
}

bool PixelHistoryDestroyResources(WrappedOpenGL *driver, const GLPixelHistoryResources &resources)
{
  driver->glDeleteTextures(1, &resources.fullPrecisionColorImage);
  driver->glDeleteTextures(1, &resources.fullPrecisionDsImage);
  driver->glDeleteFramebuffers(1, &resources.copySourceFramebuffer);
  driver->glDeleteFramebuffers(1, &resources.fullPrecisionFrameBuffer);
  driver->glDeleteShader(resources.primitiveIdFragmentShader);
  driver->glDeleteShader(resources.primitiveIdFragmentShaderSPIRV);
  driver->glDeleteProgram(resources.msCopyComputeProgram);
  driver->glDeleteProgram(resources.msCopyDepthComputeProgram);
  driver->glDeleteBuffers(1, &resources.msCopyDstBuffer);
  driver->glDeleteBuffers(1, &resources.msCopyUniformBlockBuffer);

  for(const std::pair<const GLuint, GLuint> &resourceProgram : resources.primIdPrograms)
    driver->glDeleteProgram(resourceProgram.second);

  for(const std::pair<const GLuint, GLuint> &resourceProgram : resources.fixedColPrograms)
    driver->glDeleteProgram(resourceProgram.second);

  for(const auto &pair : resources.copyFramebuffers)
  {
    const CopyFramebuffer &cf = pair.second;
    driver->glDeleteFramebuffers(1, &cf.framebufferId);
    driver->glDeleteTextures(1, &cf.colorTextureId);
    driver->glDeleteTextures(1, &cf.dsTextureId);
    driver->glDeleteTextures(1, &cf.depthTextureId);
    driver->glDeleteTextures(1, &cf.stencilTextureId);
    driver->glDeleteTextures(1, &cf.stencilViewId);
  }

  for(const auto &pair : resources.shaderOutIntFramebuffers)
  {
    const ShaderOutFramebuffer &f = pair.second;
    driver->glDeleteFramebuffers(1, &f.framebufferId);
    driver->glDeleteTextures(1, &f.colorTextureId);
    driver->glDeleteTextures(1, &f.dsTextureId);
  }

  return true;
}

void CopyMSSample(WrappedOpenGL *driver, const GLPixelHistoryResources &resources,
                  const CopyFramebuffer &copyFramebuffer, int sampleIdx, int x, int y,
                  float *pixelDst)
{
  GLint savedProgram;
  driver->glGetIntegerv(eGL_CURRENT_PROGRAM, &savedProgram);
  GLint savedActiveTexture;
  driver->glGetIntegerv(eGL_ACTIVE_TEXTURE, &savedActiveTexture);
  GLint savedShaderStorageBuffer;
  driver->glGetIntegerv(eGL_SHADER_STORAGE_BUFFER_BINDING, &savedShaderStorageBuffer);
  GLint savedUniformBuffer;
  driver->glGetIntegerv(eGL_UNIFORM_BUFFER_BINDING, &savedUniformBuffer);

  struct Bufbind
  {
    GLuint buf;
    GLuint64 start;
    GLuint64 size;
  } ubo3, ssbo2;

  GL.glGetIntegeri_v(eGL_UNIFORM_BUFFER_BINDING, 3, (GLint *)&ubo3.buf);
  GL.glGetInteger64i_v(eGL_UNIFORM_BUFFER_START, 3, (GLint64 *)&ubo3.start);
  GL.glGetInteger64i_v(eGL_UNIFORM_BUFFER_SIZE, 3, (GLint64 *)&ubo3.size);

  GL.glGetIntegeri_v(eGL_SHADER_STORAGE_BUFFER_BINDING, 2, (GLint *)&ssbo2.buf);
  GL.glGetInteger64i_v(eGL_SHADER_STORAGE_BUFFER_START, 2, (GLint64 *)&ssbo2.start);
  GL.glGetInteger64i_v(eGL_SHADER_STORAGE_BUFFER_SIZE, 2, (GLint64 *)&ssbo2.size);

  driver->glUseProgram(resources.msCopyComputeProgram);
  GLint srcMSLoc = driver->glGetUniformLocation(resources.msCopyComputeProgram, "srcMS");
  driver->glUniform1i(srcMSLoc, 0);

  uint32_t uniforms[4] = {uint32_t(sampleIdx), uint32_t(x), uint32_t(y),
                          0};    // { sampleIdx, x, y, dstOffset }
  driver->glBindBuffer(eGL_UNIFORM_BUFFER, resources.msCopyUniformBlockBuffer);
  driver->glNamedBufferSubDataEXT(resources.msCopyUniformBlockBuffer, 0, sizeof(uniforms), uniforms);

  driver->glBindBufferBase(eGL_UNIFORM_BUFFER, 3, resources.msCopyUniformBlockBuffer);

  driver->glActiveTexture(eGL_TEXTURE0);
  GLint savedMSTexture0;
  driver->glGetIntegerv(eGL_TEXTURE_BINDING_2D_MULTISAMPLE, &savedMSTexture0);
  driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, copyFramebuffer.colorTextureId);

  driver->glBindBuffer(eGL_SHADER_STORAGE_BUFFER, resources.msCopyDstBuffer);
  driver->glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 2, resources.msCopyDstBuffer);

  driver->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
  driver->glDispatchCompute(1, 1, 1);

  driver->glUseProgram(resources.msCopyDepthComputeProgram);
  GLint depthMSLoc = driver->glGetUniformLocation(resources.msCopyDepthComputeProgram, "depthMS");
  GLint stencilMSLoc =
      driver->glGetUniformLocation(resources.msCopyDepthComputeProgram, "stencilMS");

  driver->glUniform1i(depthMSLoc, 0);
  driver->glUniform1i(stencilMSLoc, 1);

  // { sampleIdx, x, y, dstOffset, hasDepth, hasStencil }
  uint32_t newUniforms[6] = {
      uint32_t(sampleIdx),
      uint32_t(x),
      uint32_t(y),
      1,
      copyFramebuffer.dsTextureId != 0 || copyFramebuffer.depthTextureId != 0,
      copyFramebuffer.dsTextureId != 0 || copyFramebuffer.stencilTextureId != 0,
  };
  driver->glNamedBufferSubDataEXT(resources.msCopyUniformBlockBuffer, 0, sizeof(newUniforms),
                                  newUniforms);

  driver->glActiveTexture(eGL_TEXTURE1);
  GLint savedMSTexture1;
  driver->glGetIntegerv(eGL_TEXTURE_BINDING_2D_MULTISAMPLE, &savedMSTexture1);

  if(copyFramebuffer.dsTextureId)
  {
    driver->glActiveTexture(eGL_TEXTURE0);
    driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, copyFramebuffer.dsTextureId);
    driver->glTexParameteri(eGL_TEXTURE_2D_MULTISAMPLE, eGL_DEPTH_STENCIL_TEXTURE_MODE,
                            eGL_DEPTH_COMPONENT);

    driver->glActiveTexture(eGL_TEXTURE1);
    driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, copyFramebuffer.stencilViewId);
    driver->glTexParameteri(eGL_TEXTURE_2D_MULTISAMPLE, eGL_DEPTH_STENCIL_TEXTURE_MODE,
                            eGL_STENCIL_INDEX);
  }
  else
  {
    driver->glActiveTexture(eGL_TEXTURE0);
    driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, copyFramebuffer.depthTextureId);

    driver->glActiveTexture(eGL_TEXTURE1);
    driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, copyFramebuffer.stencilTextureId);
  }

  driver->glDispatchCompute(1, 1, 1);
  driver->glMemoryBarrier(eGL_SHADER_STORAGE_BARRIER_BIT);

  driver->glGetBufferSubData(eGL_SHADER_STORAGE_BUFFER, 0, 8 * 4, pixelDst);

  if(ubo3.buf == 0 || (ubo3.start == 0 && ubo3.size == 0))
    GL.glBindBufferBase(eGL_UNIFORM_BUFFER, 3, ubo3.buf);
  else
    GL.glBindBufferRange(eGL_UNIFORM_BUFFER, 3, ubo3.buf, (GLintptr)ubo3.start,
                         (GLsizeiptr)ubo3.size);

  if(ssbo2.buf == 0 || (ssbo2.start == 0 && ssbo2.size == 0))
    GL.glBindBufferBase(eGL_SHADER_STORAGE_BUFFER, 2, ssbo2.buf);
  else
    GL.glBindBufferRange(eGL_SHADER_STORAGE_BUFFER, 2, ssbo2.buf, (GLintptr)ssbo2.start,
                         (GLsizeiptr)ssbo2.size);

  driver->glUseProgram(savedProgram);
  driver->glActiveTexture(eGL_TEXTURE0);
  driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, savedMSTexture0);
  driver->glActiveTexture(eGL_TEXTURE1);
  driver->glBindTexture(eGL_TEXTURE_2D_MULTISAMPLE, savedMSTexture1);
  driver->glActiveTexture((GLenum)savedActiveTexture);
  driver->glBindBuffer(eGL_SHADER_STORAGE_BUFFER, savedShaderStorageBuffer);
  driver->glBindBuffer(eGL_UNIFORM_BUFFER, savedUniformBuffer);
}

rdcarray<EventUsage> QueryModifyingEvents(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                                          const rdcarray<EventUsage> &events, int x, int y,
                                          int mipLevel, rdcarray<PixelModification> &history)
{
  GLMarkerRegion region("QueryModifyingEvents");
  rdcarray<EventUsage> modEvents;
  rdcarray<GLuint> occlusionQueries;
  std::set<uint32_t> ignoredEvents;
  occlusionQueries.resize(events.size());
  driver->glGenQueries((GLsizei)occlusionQueries.size(), occlusionQueries.data());

  driver->ReplayLog(0, events[0].eventId, eReplay_WithoutDraw);
  // execute the occlusion queries
  for(size_t i = 0; i < events.size(); i++)
  {
    uint32_t colIdx = getFramebufferColIndex(driver, resources.target);

    bool ignored = false;
    int objectType;
    driver->glGetFramebufferAttachmentParameteriv(
        eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
        eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
    // Ignore the event if the framebuffer is attached to a different mip level than the one we're
    // interested in
    if(objectType == eGL_TEXTURE && events[i].usage != ResourceUsage::Clear &&
       !isDirectWrite(events[i].usage))
    {
      int attachedMipLevel;
      driver->glGetFramebufferAttachmentParameteriv(
          eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
          eGL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &attachedMipLevel);
      if(mipLevel != attachedMipLevel)
      {
        ignoredEvents.insert(events[i].eventId);
        ignored = true;
      }
    }
    if(!ignored && !(events[i].usage == ResourceUsage::Clear || isDirectWrite(events[i].usage)))
    {
      GLRenderState state;
      state.FetchState(driver);

      GLint currentProgram = 0;
      driver->glGetIntegerv(eGL_CURRENT_PROGRAM, &currentProgram);

      if(!IsGLES)
        driver->glEnable(eGL_DEPTH_CLAMP);
      if(HasExt[EXT_depth_bounds_test])
        driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
      driver->glDisable(eGL_DEPTH_TEST);
      driver->glDisable(eGL_STENCIL_TEST);
      driver->glDisable(eGL_CULL_FACE);
      driver->glDisable(eGL_SAMPLE_MASK);
      driver->glEnable(eGL_SCISSOR_TEST);
      driver->glScissor(x, y, 1, 1);
      driver->glUseProgram(GetFixedColProgram(driver, driver->GetReplay(), resources, currentProgram));

      driver->SetFetchCounters(true);
      driver->glBeginQuery(eGL_SAMPLES_PASSED, occlusionQueries[i]);
      driver->ReplayLog(events[i].eventId, events[i].eventId, eReplay_OnlyDraw);
      driver->glEndQuery(eGL_SAMPLES_PASSED);
      driver->SetFetchCounters(false);

      state.ApplyState(driver);
    }
    else
    {
      driver->ReplayLog(events[i].eventId, events[i].eventId, eReplay_OnlyDraw);
    }

    if(i < events.size() - 1)
    {
      driver->ReplayLog(events[i].eventId + 1, events[i + 1].eventId, eReplay_WithoutDraw);
    }
  }
  // read back the occlusion queries and generate the list of potentially modifying events
  for(size_t i = 0; i < events.size(); i++)
  {
    if(ignoredEvents.count(events[i].eventId) == 1)
    {
      continue;
    }
    if(events[i].usage == ResourceUsage::Clear || isDirectWrite(events[i].usage))
    {
      PixelModification mod;
      RDCEraseEl(mod);
      mod.eventId = events[i].eventId;
      mod.directShaderWrite = isDirectWrite(events[i].usage);
      history.push_back(mod);

      modEvents.push_back(events[i]);
    }
    else
    {
      uint32_t numSamples;
      driver->glGetQueryObjectuiv(occlusionQueries[i], eGL_QUERY_RESULT, &numSamples);

      if(numSamples > 0)
      {
        PixelModification mod;
        RDCEraseEl(mod);
        mod.eventId = events[i].eventId;
        history.push_back(mod);
        modEvents.push_back(events[i]);
      }
    }
  }

  driver->glDeleteQueries((GLsizei)occlusionQueries.size(), occlusionQueries.data());
  return modEvents;
}

struct ScopedReadPixelsSanitiser
{
  PixelUnpackState unpack;
  PixelPackState pack;
  GLuint pixelPackBuffer = 0;
  GLuint pixelUnpackBuffer = 0;

  ScopedReadPixelsSanitiser()
  {
    unpack.Fetch(false);
    pack.Fetch(false);

    GL.glGetIntegerv(eGL_PIXEL_PACK_BUFFER_BINDING, (GLint *)&pixelPackBuffer);
    GL.glGetIntegerv(eGL_PIXEL_UNPACK_BUFFER_BINDING, (GLint *)&pixelUnpackBuffer);

    ResetPixelPackState(false, 1);
    ResetPixelUnpackState(false, 1);
    GL.glBindBuffer(eGL_PIXEL_PACK_BUFFER, 0);
    GL.glBindBuffer(eGL_PIXEL_UNPACK_BUFFER, 0);
  }

  ~ScopedReadPixelsSanitiser()
  {
    unpack.Apply(false);
    pack.Apply(false);
    GL.glBindBuffer(eGL_PIXEL_PACK_BUFFER, pixelPackBuffer);
    GL.glBindBuffer(eGL_PIXEL_UNPACK_BUFFER, pixelUnpackBuffer);
  }
};

void readPixelValues(WrappedOpenGL *driver, const GLPixelHistoryResources &resources,
                     const CopyFramebuffer &copyFramebuffer, rdcarray<PixelModification> &history,
                     int historyIndex, ModType modType, bool readStencil, uint32_t numPixels)
{
  ScopedReadPixelsSanitiser scope;

  GLenum colourFormatType = getTextureFormatType(copyFramebuffer.format.colorFormat);

  driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, copyFramebuffer.framebufferId);
  rdcarray<uint32_t> intColourValues;
  intColourValues.resize(4 * numPixels);
  rdcarray<float> floatColourValues;
  floatColourValues.resize(4 * numPixels);
  rdcarray<float> depthValues;
  depthValues.resize(numPixels);
  rdcarray<int> stencilValues;
  stencilValues.resize(numPixels);
  if(colourFormatType == eGL_UNSIGNED_INT)
  {
    driver->glReadPixels(0, 0, GLint(numPixels), 1, eGL_RGBA_INTEGER, eGL_UNSIGNED_INT,
                         (void *)intColourValues.data());
  }
  else if(colourFormatType == eGL_UNSIGNED_INT)
  {
    driver->glReadPixels(0, 0, GLint(numPixels), 1, eGL_RGBA_INTEGER, eGL_INT,
                         (void *)intColourValues.data());
  }
  else
  {
    driver->glReadPixels(0, 0, GLint(numPixels), 1, eGL_RGBA, eGL_FLOAT,
                         (void *)floatColourValues.data());
    if(IsSRGBFormat(copyFramebuffer.format.colorFormat))
      for(float &f : floatColourValues)
        f = ConvertSRGBToLinear(f);
  }
  if(copyFramebuffer.dsTextureId != 0 || copyFramebuffer.depthTextureId != 0)
  {
    driver->glReadPixels(0, 0, GLint(numPixels), 1, eGL_DEPTH_COMPONENT, eGL_FLOAT,
                         (void *)depthValues.data());
  }

  if(copyFramebuffer.dsTextureId != 0 || copyFramebuffer.stencilTextureId != 0)
  {
    driver->glReadPixels(0, 0, GLint(numPixels), 1, eGL_STENCIL_INDEX, eGL_INT,
                         (void *)stencilValues.data());
  }

  for(size_t i = 0; i < numPixels; i++)
  {
    ModificationValue modValue;

    for(int j = 0; j < 4; ++j)
    {
      if(colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_UNSIGNED_INT)
      {
        modValue.col.uintValue[j] = intColourValues[i * 4 + j];
      }
      else
      {
        modValue.col.floatValue[j] = floatColourValues[i * 4 + j];
      }
    }

    const ActionDescription *action = driver->GetAction(history[historyIndex + i].eventId);
    if(action->flags & ActionFlags::ClearColor)
    {
      modValue.depth = -1;
      modValue.stencil = -1;
    }
    else if(history[historyIndex + i].directShaderWrite && !resources.depthTarget)
    {
      modValue.depth = -1;
      if(historyIndex + i + 1 < history.size() &&
         history[historyIndex + i].eventId == history[historyIndex + i + 1].eventId)
        modValue.stencil = -2;
      else
        modValue.stencil = -1;
    }
    else
    {
      if(action->flags & ActionFlags::ClearDepthStencil)
      {
        RDCEraseEl(modValue.col);
      }

      modValue.depth = depthValues[i];
      if(readStencil)
      {
        modValue.stencil = stencilValues[i];
      }
      else
      {
        // if this isn't the last fragment in this event, the postmod stencil is unavailable
        if(modType == ModType::PostMod && historyIndex + i + 1 < history.size() &&
           history[historyIndex + i].eventId == history[historyIndex + i + 1].eventId)
          modValue.stencil = -2;
        // if this isn't the first fragment in this event, the premod stencil is unavailable
        else if(modType == ModType::PreMod && historyIndex + i > 0 &&
                history[historyIndex + i].eventId == history[historyIndex + i - 1].eventId)
          modValue.stencil = -2;
        else
          modValue.stencil = modType == ModType::PreMod ? history[historyIndex + i].preMod.stencil
                                                        : history[historyIndex + i].postMod.stencil;
      }
    }

    if(modType == ModType::PreMod)
      history[historyIndex + i].preMod = modValue;
    else
      history[historyIndex + i].postMod = modValue;
  }
}

void readPixelValuesMS(WrappedOpenGL *driver, const GLPixelHistoryResources &resources,
                       const CopyFramebuffer &copyFramebuffer, int sampleIdx, int x, int y,
                       rdcarray<PixelModification> &history, size_t historyIndex, ModType modType,
                       bool readStencil)
{
  rdcarray<float> pixelValue;
  pixelValue.resize(8);
  CopyMSSample(driver, resources, copyFramebuffer, sampleIdx, x, y, pixelValue.data());

  const int depthOffset = 4;
  const int stencilOffset = 5;
  ModificationValue &modValue =
      modType == ModType::PreMod ? history[historyIndex].preMod : history[historyIndex].postMod;

  for(int j = 0; j < 4; ++j)
  {
    modValue.col.floatValue[j] = pixelValue[j];
  }

  const ActionDescription *action = driver->GetAction(history[historyIndex].eventId);
  if(action->flags & ActionFlags::ClearColor)
  {
    modValue.depth = -1;
    modValue.stencil = -1;
  }
  else if(history[historyIndex].directShaderWrite && !resources.depthTarget)
  {
    modValue.depth = -1;
    if(historyIndex + 1 < history.size() &&
       history[historyIndex].eventId == history[historyIndex + 1].eventId)
      modValue.stencil = -2;
    else
      modValue.stencil = -1;
  }
  else
  {
    if(action->flags & ActionFlags::ClearDepthStencil)
    {
      RDCEraseEl(modValue.col);
    }

    modValue.depth = pixelValue[depthOffset];
    if(readStencil)
    {
      modValue.stencil = *(int *)&pixelValue[stencilOffset];
    }
    else
    {
      // if this isn't the last fragment in this event, the postmod stencil is unavailable
      if(modType == ModType::PostMod && historyIndex + 1 < history.size() &&
         history[historyIndex].eventId == history[historyIndex + 1].eventId)
        modValue.stencil = -2;
      // if this isn't the first fragment in this event, the premod stencil is unavailable
      else if(modType == ModType::PreMod && historyIndex > 0 &&
              history[historyIndex].eventId == history[historyIndex - 1].eventId)
        modValue.stencil = -2;
    }
  }
}

void QueryPrePostModPixelValues(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                                const rdcarray<EventUsage> &modEvents, int x, int y,
                                rdcarray<PixelModification> &history, uint32_t numSamples,
                                uint32_t sampleIndex)
{
  GLMarkerRegion region("QueryPrePostModPixelValues");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);
  CopyFramebuffer premodCopyFramebuffer = {};
  premodCopyFramebuffer.framebufferId = ~0u;
  CopyFramebuffer postmodCopyFramebuffer = {};
  postmodCopyFramebuffer.framebufferId = ~0u;
  int preModLastIdx = 0, postModLastIdx = 0;

  for(size_t i = 0; i < modEvents.size(); i++)
  {
    uint32_t colIdx = getFramebufferColIndex(driver, resources.target);

    {
      GLint savedReadFramebuffer, savedDrawFramebuffer;
      driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);
      driver->glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, &savedReadFramebuffer);

      GLuint copySourceFramebuffer = savedDrawFramebuffer;

      if(isDirectWrite(modEvents[i].usage))
      {
        copySourceFramebuffer = resources.copySourceFramebuffer;

        // bind this as the framebuffer so the below code which expects to duplicate based on bound
        // framebuffer will work. We've already saved teh real one
        driver->glBindFramebuffer(eGL_FRAMEBUFFER, copySourceFramebuffer);
      }

      if(numSamples > 1)
      {
        premodCopyFramebuffer =
            getCopyFramebuffer(driver, colIdx, resources.copyFramebuffers, ModType::PreMod,
                               numSamples, int(modEvents.size()));

        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, premodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, copySourceFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);
        readPixelValuesMS(driver, resources, premodCopyFramebuffer, sampleIndex, 0, 0, history, i,
                          ModType::PreMod, true);

        driver->glReadBuffer(savedReadBuffer);
      }
      else
      {
        CopyFramebuffer newCopyFramebuffer =
            getCopyFramebuffer(driver, colIdx, resources.copyFramebuffers, ModType::PreMod,
                               1 /*single sampled*/, int(modEvents.size()));
        if(newCopyFramebuffer.framebufferId != premodCopyFramebuffer.framebufferId)
        {
          if(premodCopyFramebuffer.framebufferId != ~0u)
          {
            readPixelValues(driver, resources, premodCopyFramebuffer, history, preModLastIdx,
                            ModType::PreMod, true, (uint32_t)(i - preModLastIdx));
          }
          preModLastIdx = int(i);
        }

        premodCopyFramebuffer = newCopyFramebuffer;
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, premodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, copySourceFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, GLint(i) - preModLastIdx, 0,
                            GLint(i) + 1 - preModLastIdx, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);

        driver->glReadBuffer(savedReadBuffer);
      }

      // restore the capture's framebuffer
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);
    }

    driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

    {
      GLint savedReadFramebuffer, savedDrawFramebuffer;
      driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);
      driver->glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, &savedReadFramebuffer);

      GLuint copySourceFramebuffer = savedDrawFramebuffer;

      if(isDirectWrite(modEvents[i].usage))
      {
        copySourceFramebuffer = resources.copySourceFramebuffer;

        // bind this as the framebuffer so the below code which expects to duplicate based on bound
        // framebuffer will work. We've already saved teh real one
        driver->glBindFramebuffer(eGL_FRAMEBUFFER, copySourceFramebuffer);
      }

      if(numSamples > 1)
      {
        postmodCopyFramebuffer =
            getCopyFramebuffer(driver, colIdx, resources.copyFramebuffers, ModType::PostMod,
                               numSamples, int(modEvents.size()));

        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, postmodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, copySourceFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);
        readPixelValuesMS(driver, resources, postmodCopyFramebuffer, sampleIndex, 0, 0, history, i,
                          ModType::PostMod, true);

        driver->glReadBuffer(savedReadBuffer);
      }
      else
      {
        CopyFramebuffer newCopyFramebuffer =
            getCopyFramebuffer(driver, colIdx, resources.copyFramebuffers, ModType::PostMod,
                               1 /*single sampled*/, int(modEvents.size()));
        if(newCopyFramebuffer.framebufferId != postmodCopyFramebuffer.framebufferId)
        {
          if(postmodCopyFramebuffer.framebufferId != ~0u)
          {
            readPixelValues(driver, resources, postmodCopyFramebuffer, history, postModLastIdx,
                            ModType::PostMod, true, (uint32_t)(i - postModLastIdx));
          }
          postModLastIdx = int(i);
        }

        postmodCopyFramebuffer = newCopyFramebuffer;
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, postmodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, copySourceFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, GLint(i) - postModLastIdx, 0,
                            GLint(i) + 1 - postModLastIdx, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);

        driver->glReadBuffer(savedReadBuffer);
      }

      // restore the capture's framebuffer
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);
    }

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId + 1, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }

  if(numSamples == 1 && premodCopyFramebuffer.framebufferId != 0u)
  {
    readPixelValues(driver, resources, premodCopyFramebuffer, history, preModLastIdx,
                    ModType::PreMod, true, int(modEvents.size()) - preModLastIdx);
  }
  if(numSamples == 1 && postmodCopyFramebuffer.framebufferId != 0u)
  {
    readPixelValues(driver, resources, postmodCopyFramebuffer, history, postModLastIdx,
                    ModType::PostMod, true, int(modEvents.size()) - postModLastIdx);
  }
}

ModificationValue readShaderOutMS(WrappedOpenGL *driver, const GLPixelHistoryResources &resources,
                                  const CopyFramebuffer &copyFramebuffer, int sampleIdx, int x, int y)
{
  rdcarray<float> pixelValue;
  pixelValue.resize(8);
  CopyMSSample(driver, resources, copyFramebuffer, sampleIdx, x, y, pixelValue.data());

  const int depthOffset = 4;
  const int stencilOffset = 5;
  ModificationValue modValue;

  for(int j = 0; j < 4; ++j)
  {
    modValue.col.floatValue[j] = pixelValue[j];
  }
  modValue.depth = pixelValue[depthOffset];
  modValue.stencil = *(int *)&pixelValue[stencilOffset];
  return modValue;
}

struct EventFragmentData
{
  std::map<uint32_t, uint32_t> eventFragments;
  std::map<uint32_t, GLbitfield> eventFBMask;
  rdcarray<uint32_t> fragmentsClipped;
};

// This function gets:
//  - calculates the number of fragments per event
//  - per-event whether some fragments were discarded
//  - a per-event mask of which of colour/depth is bound and valid
//  - reads the shader output values per event (per-fragment shader output is fetched later)
EventFragmentData QueryNumFragmentsByEvent(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                                           const rdcarray<EventUsage> &modEvents,
                                           rdcarray<PixelModification> &history, int x, int y,
                                           uint32_t numSamples, uint32_t sampleIndex,
                                           uint32_t width, uint32_t height)
{
  GLMarkerRegion region("QueryNumFragmentsByEvent");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);

  EventFragmentData ret;
  std::map<uint32_t, uint32_t> &eventFragments = ret.eventFragments;
  std::map<uint32_t, GLbitfield> &eventFBMask = ret.eventFBMask;
  rdcarray<uint32_t> &fragmentsClipped = ret.fragmentsClipped;

  for(size_t i = 0; i < modEvents.size(); ++i)
  {
    GLRenderState state;
    state.FetchState(driver);

    eventFBMask[modEvents[i].eventId] = getFramebufferCopyMask(driver);

    uint32_t colIdx = getFramebufferColIndex(driver, resources.target);

    GLenum colourFormat = getCurrentTextureFormat(driver, colIdx);
    GLenum colourFormatType = getTextureFormatType(colourFormat);
    GLenum shaderOutColourFormat = eGL_NONE;

    if(colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_INT)
    {
      shaderOutColourFormat = colourFormatType == eGL_UNSIGNED_INT ? eGL_RGBA32UI : eGL_RGBA32I;
      FramebufferKey key = {ModType::PostMod, shaderOutColourFormat, eGL_DEPTH32F_STENCIL8,
                            eGL_DEPTH32F_STENCIL8, numSamples};
      ShaderOutFramebuffer framebuffer =
          getShaderOutFramebuffer(driver, colIdx, resources, key, width, height);
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, framebuffer.framebufferId);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, framebuffer.framebufferId);
      glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
    }
    else
    {
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
      for(uint32_t a = 0; a < 8; a++)
        driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + a), 0, 0);
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                                   resources.fullPrecisionColorImage, 0);
      glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
    }

    rdcarray<GLenum> drawBufs;
    drawBufs.resize(colIdx);
    drawBufs.push_back(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
    driver->glDrawBuffers((GLsizei)drawBufs.size(), drawBufs.data());

    driver->glStencilOp(eGL_INCR, eGL_INCR, eGL_INCR);
    driver->glStencilMask(0xff);    // default for 1 byte
    driver->glStencilFunc(eGL_ALWAYS, 0, 0xff);
    driver->glClearStencil(0);
    driver->glClearColor(0, 0, 0, 0);
    if(driver->isGLESMode())
    {
      driver->glClearDepthf(0.0f);
    }
    else
    {
      driver->glClearDepth(0);
    }
    driver->glClear(eGL_STENCIL_BUFFER_BIT | eGL_COLOR_BUFFER_BIT | eGL_DEPTH_BUFFER_BIT);
    driver->glEnable(eGL_STENCIL_TEST);
    // depth test enable
    driver->glEnable(eGL_DEPTH_TEST);
    if(HasExt[EXT_depth_bounds_test])
      driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
    driver->glDepthFunc(eGL_ALWAYS);
    driver->glDepthMask(GL_TRUE);
    driver->glDisable(eGL_BLEND);
    driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

    // enable the sample we're looking at so we get the shaderOut even if it's masked off
    driver->glEnable(eGL_SAMPLE_MASK);

    driver->glSampleMaski(0, 1u << sampleIndex);

    // replay event
    driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

    uint32_t numFragments = 0;
    uint32_t numFragmentsWithoutDiscard = 0;

    if(modEvents[i].usage != ResourceUsage::Clear && !isDirectWrite(modEvents[i].usage))
    {
      if(numSamples == 1)
      {
        ScopedReadPixelsSanitiser scope;

        ModificationValue modValue;
        if(colourFormatType == eGL_UNSIGNED_INT)
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA_INTEGER, eGL_UNSIGNED_INT,
                               (void *)modValue.col.uintValue.data());
        }
        else if(colourFormatType == eGL_INT)
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA_INTEGER, eGL_INT,
                               (void *)modValue.col.intValue.data());
        }
        else
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA, eGL_FLOAT,
                               (void *)modValue.col.floatValue.data());
        }
        driver->glReadPixels(x, y, 1, 1, eGL_DEPTH_COMPONENT, eGL_FLOAT, (void *)&modValue.depth);
        driver->glReadPixels(x, y, 1, 1, eGL_STENCIL_INDEX, eGL_UNSIGNED_INT, (void *)&numFragments);

        // We're not reading the stencil value here, so use the postMod instead.
        // Shaders don't actually output stencil values, those are determined by the stencil op.
        modValue.stencil = history[i].postMod.stencil;

        history[i].shaderOut = modValue;

        if(resources.depthTarget)
          RDCEraseEl(history[i].shaderOut.col);
      }
      else
      {
        GLenum copyFramebufferColourFormat =
            (colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_INT)
                ? shaderOutColourFormat
                : eGL_RGBA32F;

        const CopyFramebuffer &copyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, copyFramebufferColourFormat);

        GLint savedDrawFramebuffer;
        driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);

        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, copyFramebuffer.framebufferId);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        glReadBuffer(eGL_COLOR_ATTACHMENT0);
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);

        driver->glReadBuffer(savedReadBuffer);
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);

        history[i].shaderOut = readShaderOutMS(driver, resources, copyFramebuffer, sampleIndex, 0, 0);
        numFragments = history[i].shaderOut.stencil;
        history[i].shaderOut.stencil = history[i].postMod.stencil;

        if(resources.depthTarget)
          RDCEraseEl(history[i].shaderOut.col);
      }

      GLint currentProgram = 0;
      driver->glGetIntegerv(eGL_CURRENT_PROGRAM, &currentProgram);

      // re-run draw with program that outputs a fixed colour to count how many fragments we get without discards
      driver->glUseProgram(GetFixedColProgram(driver, driver->GetReplay(), resources, currentProgram));
      driver->glClear(eGL_STENCIL_BUFFER_BIT | eGL_COLOR_BUFFER_BIT | eGL_DEPTH_BUFFER_BIT);
      driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

      if(numSamples == 1)
      {
        driver->glReadPixels(x, y, 1, 1, eGL_STENCIL_INDEX, eGL_UNSIGNED_INT,
                             (void *)&numFragmentsWithoutDiscard);
      }
      else
      {
        GLenum copyFramebufferColourFormat =
            (colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_INT)
                ? shaderOutColourFormat
                : eGL_RGBA32F;

        const CopyFramebuffer &copyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, copyFramebufferColourFormat);
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, copyFramebuffer.framebufferId);
        glReadBuffer(eGL_COLOR_ATTACHMENT0);
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);
        numFragmentsWithoutDiscard =
            readShaderOutMS(driver, resources, copyFramebuffer, sampleIndex, 0, 0).stencil;
      }
    }

    if(modEvents[i].usage == ResourceUsage::Clear)
    {
      history[i].shaderOut = history[i].postMod;
      if(resources.depthTarget)
        RDCEraseEl(history[i].shaderOut.col);
    }

    if(isDirectWrite(modEvents[i].usage))
    {
      RDCEraseEl(history[i].shaderOut);
      history[i].shaderOut.depth = -1;
      history[i].shaderOut.stencil = -1;
    }

    eventFragments.emplace(modEvents[i].eventId, numFragmentsWithoutDiscard);
    if(numFragmentsWithoutDiscard > numFragments)
      fragmentsClipped.push_back(modEvents[i].eventId);

    state.ApplyState(driver);

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId + 1, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }

  return ret;
}

bool QueryScissorTest(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                      const EventUsage event, int x, int y)
{
  driver->ReplayLog(0, event.eventId, eReplay_WithoutDraw);
  bool scissorTestFailed = false;
  if(driver->glIsEnabled(eGL_SCISSOR_TEST))
  {
    int scissorBox[4];    // [x, y, width, height]
    driver->glGetIntegerv(eGL_SCISSOR_BOX, scissorBox);
    scissorTestFailed = x < scissorBox[0] || x - scissorBox[0] >= scissorBox[2] ||
                        y < scissorBox[1] || y - scissorBox[1] >= scissorBox[3];
  }
  return scissorTestFailed;
}

bool QueryTest(WrappedOpenGL *driver, GLPixelHistoryResources &resources, const EventUsage event,
               int x, int y, OpenGLTest test, uint32_t sampleIndex)
{
  driver->ReplayLog(0, event.eventId, eReplay_WithoutDraw);
  GLuint samplesPassedQuery;
  driver->glGenQueries(1, &samplesPassedQuery);
  driver->glEnable(eGL_SCISSOR_TEST);
  driver->glScissor(x, y, 1, 1);
  if(test < OpenGLTest::DepthTest)
  {
    driver->glDisable(eGL_DEPTH_TEST);
  }
  if(test < OpenGLTest::DepthClamp)
  {
    if(!IsGLES)
      driver->glEnable(eGL_DEPTH_CLAMP);
  }
  if(test < OpenGLTest::DepthBounds)
  {
    if(HasExt[EXT_depth_bounds_test])
      driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
  }
  if(test < OpenGLTest::Discard)
  {
    GLint currentProgram = 0;
    driver->glGetIntegerv(eGL_CURRENT_PROGRAM, &currentProgram);

    driver->glUseProgram(GetFixedColProgram(driver, driver->GetReplay(), resources, currentProgram));
  }
  if(test < OpenGLTest::StencilTest)
  {
    driver->glDisable(eGL_STENCIL_TEST);
  }
  if(test < OpenGLTest::FaceCulling)
  {
    driver->glDisable(eGL_CULL_FACE);
  }
  if(test < OpenGLTest::SampleMask)
  {
    driver->glDisable(eGL_SAMPLE_MASK);
  }
  else if(test == OpenGLTest::SampleMask)
  {
    GLboolean sampleMaskEnabled = driver->glIsEnabled(eGL_SAMPLE_MASK);
    if(sampleMaskEnabled)
    {
      uint32_t currentSampleMask;
      driver->glGetIntegeri_v(eGL_SAMPLE_MASK_VALUE, 0, (GLint *)&currentSampleMask);
      uint32_t newSampleMask = currentSampleMask & (sampleIndex == ~0u ? ~0u : (1u << sampleIndex));
      driver->glSampleMaski(0, newSampleMask);
    }
    else
    {
      driver->glEnable(eGL_SAMPLE_MASK);
      driver->glSampleMaski(0, 1u << sampleIndex);
    }
  }

  driver->SetFetchCounters(true);
  driver->glBeginQuery(eGL_ANY_SAMPLES_PASSED, samplesPassedQuery);
  driver->ReplayLog(event.eventId, event.eventId, eReplay_OnlyDraw);
  driver->glEndQuery(eGL_ANY_SAMPLES_PASSED);
  driver->SetFetchCounters(false);
  int anySamplesPassed = GL_FALSE;
  driver->glGetQueryObjectiv(samplesPassedQuery, eGL_QUERY_RESULT, &anySamplesPassed);
  driver->glDeleteQueries(1, &samplesPassedQuery);
  return anySamplesPassed == GL_FALSE;
}

void QueryFailedTests(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                      const rdcarray<EventUsage> &modEvents, int x, int y,
                      rdcarray<PixelModification> &history, uint32_t sampleIndex)
{
  GLMarkerRegion region("QueryFailedTests");
  for(size_t i = 0; i < modEvents.size(); ++i)
  {
    OpenGLTest failedTest = OpenGLTest::NumTests;
    if(!isDirectWrite(modEvents[i].usage))
    {
      if(modEvents[i].usage == ResourceUsage::Clear)
      {
        bool failed = QueryScissorTest(driver, resources, modEvents[i], x, y);
        if(failed)
        {
          failedTest = OpenGLTest::ScissorTest;
        }
      }
      else
      {
        for(int test = 0; test < int(OpenGLTest::NumTests); ++test)
        {
          bool failed;
          if(test == int(OpenGLTest::ScissorTest))
          {
            failed = QueryScissorTest(driver, resources, modEvents[i], x, y);
          }
          else
          {
            failed = QueryTest(driver, resources, modEvents[i], x, y, OpenGLTest(test), sampleIndex);
          }
          if(failed)
          {
            failedTest = OpenGLTest(test);
            break;
          }
        }
      }
    }

    // ensure that history objects are one-to-one mapped with modEvent objects
    RDCASSERT(history[i].eventId == modEvents[i].eventId);
    history[i].scissorClipped = failedTest == OpenGLTest::ScissorTest;
    history[i].depthClipped = failedTest == OpenGLTest::DepthClamp;
    history[i].depthBoundsFailed = failedTest == OpenGLTest::DepthBounds;
    history[i].stencilTestFailed = failedTest == OpenGLTest::StencilTest;
    history[i].depthTestFailed = failedTest == OpenGLTest::DepthTest;
    history[i].shaderDiscarded = failedTest == OpenGLTest::Discard;
    history[i].backfaceCulled = failedTest == OpenGLTest::FaceCulling;
    history[i].sampleMasked = failedTest == OpenGLTest::SampleMask;
  }
}

void QueryShaderOutPerFragment(WrappedOpenGL *driver, GLReplay *replay,
                               GLPixelHistoryResources &resources,
                               const rdcarray<EventUsage> &modEvents, int x, int y,
                               rdcarray<PixelModification> &history,
                               const EventFragmentData &eventFragmentData, uint32_t numSamples,
                               uint32_t sampleIndex, uint32_t width, uint32_t height)
{
  const std::map<uint32_t, uint32_t> &eventFragments = eventFragmentData.eventFragments;
  const rdcarray<uint32_t> &fragmentsClipped = eventFragmentData.fragmentsClipped;

  // this is used to track if any previous fragments in the current draw
  // discarded. If so, the stencil count will be off-by-one
  uint32_t discardedOffset = 0;

  GLMarkerRegion region("QueryShaderOutPerFragment");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);
  for(size_t i = 0; i < modEvents.size(); ++i)
  {
    auto it = eventFragments.find(modEvents[i].eventId);
    uint32_t numFragments = (it != eventFragments.end()) ? it->second : 0;

    if(numFragments <= 1)
    {
      if(i < modEvents.size() - 1)
      {
        driver->ReplayLog(modEvents[i].eventId, modEvents[i + 1].eventId, eReplay_WithoutDraw);
      }
      continue;
    }

    bool someFragsClipped = fragmentsClipped.contains(modEvents[i].eventId);

    // reset discarded offset every event
    discardedOffset = 0;

    GLRenderState state;
    state.FetchState(driver);

    driver->glEnable(eGL_SCISSOR_TEST);
    driver->glScissor(x, y, 1, 1);

    driver->glClearStencil(0);
    driver->glClearColor(0, 0, 0, 0);
    driver->glClearDepth(0);
    driver->glClearStencil(0);

    driver->glEnable(eGL_DEPTH_TEST);
    if(HasExt[EXT_depth_bounds_test])
      driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
    driver->glDepthFunc(eGL_ALWAYS);
    driver->glDepthMask(GL_TRUE);
    driver->glDisable(eGL_BLEND);
    driver->glEnable(eGL_SAMPLE_MASK);
    if(sampleIndex != ~0u)
    {
      driver->glSampleMaski(0, 1u << sampleIndex);
    }
    else
    {
      driver->glSampleMaski(0, ~0u);
    }

    driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    driver->glStencilMask(0xff);
    driver->glStencilOp(eGL_INCR, eGL_INCR, eGL_INCR);
    driver->glEnable(eGL_STENCIL_TEST);

    // enable the sample we're looking at so we get the shaderOut even if it's masked off
    if(sampleIndex != ~0u)
    {
      driver->glSampleMaski(0, 1u << sampleIndex);
    }

    PixelModification referenceHistory;
    referenceHistory.eventId = modEvents[i].eventId;

    auto historyIndex =
        std::lower_bound(history.begin(), history.end(), referenceHistory,
                         [](const PixelModification &h1, const PixelModification &h2) -> bool {
                           return h1.eventId < h2.eventId;
                         });

    RDCASSERT(historyIndex != history.end());

    uint32_t colIdx = getFramebufferColIndex(driver, resources.target);

    GLenum colourFormat = getCurrentTextureFormat(driver, colIdx);
    GLenum colourFormatType = getTextureFormatType(colourFormat);
    GLenum shaderOutColourFormat = eGL_NONE;

    for(size_t j = 0; j < RDCMAX(numFragments, 1u); ++j)
    {
      //  Set the stencil function so only jth fragment will pass.
      driver->glStencilFunc(eGL_EQUAL, (int)j - discardedOffset, 0xff);

      // if some fragments clipped in this draw, we need to check to see if this
      // primitive ID was one of the ones that clipped.
      // Currently the way we do that is by drawing only that primitive
      // and doing an occlusion query
      if(someFragsClipped)
      {
        driver->glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
        driver->glDepthMask(GL_FALSE);
        driver->glDisable(eGL_STENCIL_TEST);

        GLuint samplesPassedQuery;
        driver->glGenQueries(1, &samplesPassedQuery);

        driver->SetFetchCounters(true);
        driver->glBeginQuery(eGL_ANY_SAMPLES_PASSED, samplesPassedQuery);

        const ActionDescription *action = driver->GetAction(modEvents[i].eventId);
        const GLDrawParams &drawParams = driver->GetDrawParameters(modEvents[i].eventId);

        Topology topo = drawParams.topo;
        GLenum glTopo = MakeGLPrimitiveTopology(topo);
        uint32_t numVerts = RENDERDOC_NumVerticesPerPrimitive(topo);

        if(action->flags & ActionFlags::Indexed)
        {
          GLenum idxType = eGL_UNSIGNED_INT;
          if(drawParams.indexWidth == 1)
            idxType = eGL_UNSIGNED_BYTE;
          else if(drawParams.indexWidth == 2)
            idxType = eGL_UNSIGNED_SHORT;

          void *indexOffset =
              (void *)(uintptr_t)(action->indexOffset +
                                  RENDERDOC_VertexOffset(topo, historyIndex->primitiveID));

          if(action->flags & ActionFlags::Instanced)
          {
            if(HasExt[ARB_base_instance])
            {
              // TODO once pixel history distinguishes between instances, draw only the instance for
              // this fragment
              driver->glDrawElementsInstancedBaseVertexBaseInstance(
                  glTopo, numVerts, idxType, indexOffset, RDCMAX(1U, action->numInstances),
                  action->baseVertex, action->instanceOffset);
            }
            else
            {
              driver->glDrawElementsInstancedBaseVertex(glTopo, numVerts, idxType, indexOffset,
                                                        RDCMAX(1U, action->numInstances),
                                                        action->baseVertex);
            }
          }
          else
          {
            driver->glDrawElementsBaseVertex(glTopo, numVerts, idxType, indexOffset,
                                             action->baseVertex);
          }
        }
        else
        {
          uint32_t vertexOffset =
              action->vertexOffset + RENDERDOC_VertexOffset(topo, historyIndex->primitiveID);

          if(action->flags & ActionFlags::Instanced)
          {
            if(HasExt[ARB_base_instance])
            {
              // TODO once pixel history distinguishes between instances, draw only the instance for
              // this fragment
              driver->glDrawArraysInstancedBaseInstance(glTopo, vertexOffset, numVerts,
                                                        RDCMAX(1U, action->numInstances),
                                                        action->instanceOffset);
            }
            else
            {
              driver->glDrawArraysInstanced(glTopo, vertexOffset, numVerts,
                                            RDCMAX(1U, action->numInstances));
            }
          }
          else
          {
            driver->glDrawArrays(glTopo, vertexOffset, numVerts);
          }
        }

        driver->glEndQuery(eGL_ANY_SAMPLES_PASSED);
        driver->SetFetchCounters(false);
        int anySamplesPassed = GL_FALSE;
        driver->glGetQueryObjectiv(samplesPassedQuery, eGL_QUERY_RESULT, &anySamplesPassed);
        driver->glDeleteQueries(1, &samplesPassedQuery);

        driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
        driver->glDepthMask(GL_TRUE);
        driver->glEnable(eGL_STENCIL_TEST);

        if(anySamplesPassed == GL_FALSE)
        {
          historyIndex->shaderDiscarded = true;
          discardedOffset++;
          RDCEraseEl(historyIndex->shaderOut);
          historyIndex->shaderOut.depth = -1;
          if(historyIndex + 1 < history.end() && (historyIndex + 1)->eventId == historyIndex->eventId)
            historyIndex->postMod.stencil = -2;
          else
            historyIndex->postMod.stencil = -1;
          historyIndex++;
          continue;
        }
      }

      if(colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_INT)
      {
        shaderOutColourFormat = colourFormatType == eGL_UNSIGNED_INT ? eGL_RGBA32UI : eGL_RGBA32I;
        FramebufferKey key = {ModType::PostMod, shaderOutColourFormat, eGL_DEPTH32F_STENCIL8,
                              eGL_DEPTH32F_STENCIL8, numSamples};
        ShaderOutFramebuffer framebuffer =
            getShaderOutFramebuffer(driver, colIdx, resources, key, width, height);
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, framebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, framebuffer.framebufferId);
        glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
      }
      else
      {
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
        for(uint32_t a = 0; a < 8; a++)
          driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + a), 0, 0);
        driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
                                     resources.fullPrecisionColorImage, 0);
        glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
      }

      rdcarray<GLenum> drawBufs;
      drawBufs.resize(colIdx);
      drawBufs.push_back(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
      driver->glDrawBuffers((GLsizei)drawBufs.size(), drawBufs.data());

      driver->glClear(eGL_STENCIL_BUFFER_BIT | eGL_COLOR_BUFFER_BIT | eGL_DEPTH_BUFFER_BIT);

      driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

      if(numSamples == 1)
      {
        ScopedReadPixelsSanitiser scope;

        ModificationValue modValue;

        if(colourFormatType == eGL_UNSIGNED_INT)
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA_INTEGER, eGL_UNSIGNED_INT,
                               (void *)modValue.col.uintValue.data());
        }
        else if(colourFormatType == eGL_INT)
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA_INTEGER, eGL_INT,
                               (void *)modValue.col.intValue.data());
        }
        else
        {
          driver->glReadPixels(x, y, 1, 1, eGL_RGBA, eGL_FLOAT,
                               (void *)modValue.col.floatValue.data());
        }
        driver->glReadPixels(x, y, 1, 1, eGL_DEPTH_COMPONENT, eGL_FLOAT, (void *)&modValue.depth);
        modValue.stencil = historyIndex->shaderOut.stencil;

        historyIndex->shaderOut = modValue;
        if(resources.depthTarget)
          RDCEraseEl(historyIndex->shaderOut.col);
      }
      else
      {
        GLenum copyFramebufferColourFormat =
            (colourFormatType == eGL_UNSIGNED_INT || colourFormatType == eGL_INT)
                ? shaderOutColourFormat
                : eGL_RGBA32F;

        const CopyFramebuffer &copyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, copyFramebufferColourFormat);
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, copyFramebuffer.framebufferId);
        glReadBuffer(eGL_COLOR_ATTACHMENT0);
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);
        int oldStencil = historyIndex->shaderOut.stencil;
        historyIndex->shaderOut =
            readShaderOutMS(driver, resources, copyFramebuffer, sampleIndex, 0, 0);
        historyIndex->shaderOut.stencil = oldStencil;
        if(resources.depthTarget)
          RDCEraseEl(historyIndex->shaderOut.col);
      }
      historyIndex++;
    }

    state.ApplyState(driver);

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId + 1, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }
}

void QueryPrePostModPerFragment(WrappedOpenGL *driver, GLReplay *replay,
                                GLPixelHistoryResources &resources,
                                const rdcarray<EventUsage> &modEvents, int x, int y,
                                rdcarray<PixelModification> &history,
                                const EventFragmentData &eventFragmentData, uint32_t numSamples,
                                uint32_t sampleIndex, uint32_t width, uint32_t height)
{
  const std::map<uint32_t, uint32_t> &eventFragments = eventFragmentData.eventFragments;
  const rdcarray<uint32_t> &fragmentsClipped = eventFragmentData.fragmentsClipped;

  // this is used to track if any previous fragments in the current draw
  // discarded. If so, the stencil count will be off-by-one
  uint32_t discardedOffset = 0;

  GLMarkerRegion region("QueryPrePostModPerFragment");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);

  for(size_t i = 0; i < modEvents.size(); i++)
  {
    auto it = eventFragments.find(modEvents[i].eventId);
    uint32_t numFragments = (it != eventFragments.end()) ? it->second : 0;

    if(numFragments <= 1)
    {
      if(i < modEvents.size() - 1)
      {
        driver->ReplayLog(modEvents[i].eventId, modEvents[i + 1].eventId, eReplay_WithoutDraw);
      }
      continue;
    }

    bool someFragsClipped = fragmentsClipped.contains(modEvents[i].eventId);

    // reset discarded offset every event
    discardedOffset = 0;

    GLRenderState state;
    state.FetchState(driver);

    driver->glEnable(eGL_SCISSOR_TEST);
    driver->glScissor(x, y, 1, 1);

    driver->glClearStencil(0);

    driver->glStencilMask(0xff);
    driver->glStencilOp(eGL_INCR, eGL_INCR, eGL_INCR);
    driver->glEnable(eGL_STENCIL_TEST);
    driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

    PixelModification referenceHistory;
    referenceHistory.eventId = modEvents[i].eventId;

    auto historyIndex =
        std::lower_bound(history.begin(), history.end(), referenceHistory,
                         [](const PixelModification &h1, const PixelModification &h2) -> bool {
                           return h1.eventId < h2.eventId;
                         });
    RDCASSERT(historyIndex != history.end());

    GLint savedReadFramebuffer, savedDrawFramebuffer;
    driver->glGetIntegerv(eGL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFramebuffer);
    driver->glGetIntegerv(eGL_READ_FRAMEBUFFER_BINDING, &savedReadFramebuffer);

    uint32_t colIdx = getFramebufferColIndex(driver, resources.target);

    CopyFramebuffer postmodCopyFramebuffer = {};
    postmodCopyFramebuffer.framebufferId = ~0u;
    CopyFramebuffer premodCopyFramebuffer = {};
    premodCopyFramebuffer.framebufferId = ~0u;
    int preModLastJ = 0;
    int postModLastJ = 0;

    // save D/S attachment since we'll substitute our own depth
    GLuint curDepth;
    GLint curDepthType;
    GLuint curStencil;
    GLint curStencilType;
    GLint curStencilLevel = 0, curDepthLevel = 0;

    driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                  (GLint *)&curDepth);
    driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
                                                  &curDepthType);
    if(curDepth)
    {
      driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                    eGL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL,
                                                    &curDepthLevel);
    }

    driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                  (GLint *)&curStencil);
    driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                  eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
                                                  &curStencilType);
    if(curStencil)
    {
      driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                                    eGL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL,
                                                    &curStencilLevel);
    }

    // if we didn't have a depth buffer, turn off depth testing so we don't have to worry about
    // incorrect depth failures with our forced depth
    if(!curDepth)
    {
      if(HasExt[EXT_depth_bounds_test])
        driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
      driver->glDepthFunc(eGL_ALWAYS);
    }

    GLuint forcedDepth = 0;
    // replace depth and clear it to premod value
    {
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
      driver->glFramebufferTexture(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_STENCIL_ATTACHMENT, forcedDepth,
                                   0);

      GLenum textureTarget = numSamples > 1 ? eGL_TEXTURE_2D_MULTISAMPLE : eGL_TEXTURE_2D;
      driver->glGenTextures(1, &forcedDepth);
      driver->CreateTextureImage(forcedDepth, eGL_DEPTH32F_STENCIL8, eGL_NONE, eGL_NONE,
                                 textureTarget, 2, width, height, 1, numSamples, 1);
      driver->glFramebufferTexture(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_STENCIL_ATTACHMENT, forcedDepth,
                                   0);

      driver->glClearDepth(historyIndex->preMod.depth);
      driver->glClear(eGL_DEPTH_BUFFER_BIT);
    }

    GLuint curColor = 0;
    GLint colorType = 0;
    driver->glGetFramebufferAttachmentParameteriv(
        eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
        eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, (GLint *)&curColor);

    driver->glGetFramebufferAttachmentParameteriv(
        eGL_DRAW_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + colIdx),
        eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &colorType);

    GLenum colorFormat = eGL_NONE;
    if(curColor != 0)
    {
      ResourceId id;
      if(colorType != eGL_RENDERBUFFER)
      {
        id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curColor));
      }
      else
      {
        id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curColor));
      }
      colorFormat = driver->m_Textures[id].internalFormat;
    }

    for(size_t j = 0; j < std::max(numFragments, 1u); ++j)
    {
      // Set the stencil function so only jth fragment will pass.
      driver->glStencilFunc(eGL_EQUAL, (int)j - discardedOffset, 0xff);

      // if some fragments clipped in this draw, we need to check to see if this
      // primitive ID was one of the ones that clipped.
      // Currently the way we do that is by drawing only that primitive
      // and doing an occlusion query
      if(someFragsClipped)
      {
        auto curFragHistoryIndex = numSamples > 1 ? historyIndex : historyIndex + j;

        driver->glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
        driver->glDepthMask(GL_FALSE);
        driver->glDisable(eGL_STENCIL_TEST);

        GLuint samplesPassedQuery;
        driver->glGenQueries(1, &samplesPassedQuery);

        driver->SetFetchCounters(true);
        driver->glBeginQuery(eGL_ANY_SAMPLES_PASSED, samplesPassedQuery);

        const ActionDescription *action = driver->GetAction(modEvents[i].eventId);
        const GLDrawParams &drawParams = driver->GetDrawParameters(modEvents[i].eventId);

        Topology topo = drawParams.topo;
        GLenum glTopo = MakeGLPrimitiveTopology(topo);
        uint32_t numVerts = RENDERDOC_NumVerticesPerPrimitive(topo);

        if(action->flags & ActionFlags::Indexed)
        {
          GLenum idxType = eGL_UNSIGNED_INT;
          if(drawParams.indexWidth == 1)
            idxType = eGL_UNSIGNED_BYTE;
          else if(drawParams.indexWidth == 2)
            idxType = eGL_UNSIGNED_SHORT;

          void *indexOffset =
              (void *)(uintptr_t)(action->indexOffset +
                                  RENDERDOC_VertexOffset(topo, curFragHistoryIndex->primitiveID));

          if(action->flags & ActionFlags::Instanced)
          {
            if(HasExt[ARB_base_instance])
            {
              // TODO once pixel history distinguishes between instances, draw only the instance for
              // this fragment
              driver->glDrawElementsInstancedBaseVertexBaseInstance(
                  glTopo, numVerts, idxType, indexOffset, RDCMAX(1U, action->numInstances),
                  action->baseVertex, action->instanceOffset);
            }
            else
            {
              driver->glDrawElementsInstancedBaseVertex(glTopo, numVerts, idxType, indexOffset,
                                                        RDCMAX(1U, action->numInstances),
                                                        action->baseVertex);
            }
          }
          else
          {
            driver->glDrawElementsBaseVertex(glTopo, numVerts, idxType, indexOffset,
                                             action->baseVertex);
          }
        }
        else
        {
          uint32_t vertexOffset =
              action->vertexOffset + RENDERDOC_VertexOffset(topo, curFragHistoryIndex->primitiveID);

          if(action->flags & ActionFlags::Instanced)
          {
            if(HasExt[ARB_base_instance])
            {
              // TODO once pixel history distinguishes between instances, draw only the instance for
              // this fragment
              driver->glDrawArraysInstancedBaseInstance(glTopo, vertexOffset, numVerts,
                                                        RDCMAX(1U, action->numInstances),
                                                        action->instanceOffset);
            }
            else
            {
              driver->glDrawArraysInstanced(glTopo, vertexOffset, numVerts,
                                            RDCMAX(1U, action->numInstances));
            }
          }
          else
          {
            driver->glDrawArrays(glTopo, vertexOffset, numVerts);
          }
        }

        driver->glEndQuery(eGL_ANY_SAMPLES_PASSED);
        driver->SetFetchCounters(false);
        int anySamplesPassed = GL_FALSE;
        driver->glGetQueryObjectiv(samplesPassedQuery, eGL_QUERY_RESULT, &anySamplesPassed);
        driver->glDeleteQueries(1, &samplesPassedQuery);

        driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
        driver->glDepthMask(GL_TRUE);
        driver->glEnable(eGL_STENCIL_TEST);

        if(anySamplesPassed == GL_FALSE)
        {
          discardedOffset++;
          curFragHistoryIndex->postMod = curFragHistoryIndex->preMod;
          if(historyIndex + j + 1 < history.end() &&
             (curFragHistoryIndex + 1)->eventId == curFragHistoryIndex->eventId)
            curFragHistoryIndex->postMod.stencil = -2;
          else
            curFragHistoryIndex->postMod.stencil = -1;

          if(numSamples > 1)
            historyIndex++;
          continue;
        }
      }

      driver->glClear(eGL_STENCIL_BUFFER_BIT);

      if(j > 0)
      {
        if(numSamples > 1)
        {
          premodCopyFramebuffer = getCopyFramebuffer(
              driver, resources.copyFramebuffers, ModType::PreMod, numSamples,
              int(modEvents.size()), eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, colorFormat);

          driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, premodCopyFramebuffer.framebufferId);
          driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedDrawFramebuffer);

          GLenum savedReadBuffer;
          driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

          driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
          SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                              eGL_NEAREST);

          readPixelValuesMS(driver, resources, premodCopyFramebuffer, sampleIndex, 0, 0, history,
                            (historyIndex - history.begin()), ModType::PreMod, false);

          driver->glReadBuffer(savedReadBuffer);
        }
        else
        {
          // Blit the values into out framebuffer
          CopyFramebuffer newCopyFramebuffer = getCopyFramebuffer(
              driver, resources.copyFramebuffers, ModType::PreMod, numSamples,
              int(modEvents.size()), eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, colorFormat);
          if(newCopyFramebuffer.framebufferId != premodCopyFramebuffer.framebufferId ||
             (j - preModLastJ >= premodCopyFramebuffer.width))
          {
            if(premodCopyFramebuffer.framebufferId != ~0u)
            {
              readPixelValues(driver, resources, premodCopyFramebuffer, history,
                              preModLastJ + int(historyIndex - history.begin()), ModType::PreMod,
                              false, (uint32_t)(j - preModLastJ));
            }
            preModLastJ = int(j);
          }
          premodCopyFramebuffer = newCopyFramebuffer;
          driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, premodCopyFramebuffer.framebufferId);
          driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedDrawFramebuffer);

          GLenum savedReadBuffer;
          driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

          driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
          SafeBlitFramebuffer(x, y, x + 1, y + 1, GLint(j) - preModLastJ, 0,
                              GLint(j) + 1 - preModLastJ, 1, getFramebufferCopyMask(driver),
                              eGL_NEAREST);

          driver->glReadBuffer(savedReadBuffer);
        }
      }

      // restore the capture's framebuffer
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);

      driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

      if(numSamples > 1)
      {
        postmodCopyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, colorFormat);

        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, postmodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedDrawFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);

        readPixelValuesMS(driver, resources, postmodCopyFramebuffer, sampleIndex, 0, 0, history,
                          (historyIndex - history.begin()), ModType::PostMod, false);
        historyIndex++;

        driver->glReadBuffer(savedReadBuffer);
      }
      else
      {
        // Blit the values into out framebuffer
        CopyFramebuffer newCopyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, colorFormat);
        if(newCopyFramebuffer.framebufferId != postmodCopyFramebuffer.framebufferId ||
           (j - postModLastJ >= postmodCopyFramebuffer.width))
        {
          if(postmodCopyFramebuffer.framebufferId != ~0u)
          {
            readPixelValues(driver, resources, postmodCopyFramebuffer, history,
                            postModLastJ + int(historyIndex - history.begin()), ModType::PostMod,
                            false, (uint32_t)(j - postModLastJ));
          }
          postModLastJ = int(j);
        }
        postmodCopyFramebuffer = newCopyFramebuffer;
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, postmodCopyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedDrawFramebuffer);

        GLenum savedReadBuffer;
        driver->glGetIntegerv(eGL_READ_BUFFER, (GLint *)&savedReadBuffer);

        driver->glReadBuffer(GLenum(eGL_COLOR_ATTACHMENT0 + colIdx));
        SafeBlitFramebuffer(x, y, x + 1, y + 1, GLint(j) - postModLastJ, 0,
                            GLint(j) + 1 - postModLastJ, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);

        driver->glReadBuffer(savedReadBuffer);
      }

      // restore the capture's framebuffer
      driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, savedDrawFramebuffer);
      driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, savedReadFramebuffer);
    }

    // restore original D/S attachments
    if(curDepthType != eGL_RENDERBUFFER)
    {
      driver->glFramebufferTexture(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT, curDepth,
                                   curDepthLevel);
    }
    else
    {
      driver->glFramebufferRenderbuffer(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                        eGL_RENDERBUFFER, curDepth);
    }
    if(curStencilType != eGL_RENDERBUFFER)
    {
      driver->glFramebufferTexture(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT, curStencil,
                                   curStencilLevel);
    }
    else
    {
      driver->glFramebufferRenderbuffer(eGL_DRAW_FRAMEBUFFER, eGL_STENCIL_ATTACHMENT,
                                        eGL_RENDERBUFFER, curStencil);
    }

    driver->glDeleteTextures(1, &forcedDepth);

    if(numSamples == 1 && premodCopyFramebuffer.framebufferId != ~0u)
    {
      readPixelValues(driver, resources, premodCopyFramebuffer, history,
                      preModLastJ + int(historyIndex - history.begin()), ModType::PreMod, false,
                      numFragments - preModLastJ);
    }

    if(numSamples == 1 && postmodCopyFramebuffer.framebufferId != ~0u)
    {
      readPixelValues(driver, resources, postmodCopyFramebuffer, history,
                      postModLastJ + int(historyIndex - history.begin()), ModType::PostMod, false,
                      numFragments - postModLastJ);
    }

    state.ApplyState(driver);

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId + 1, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }
}

void QueryPrimitiveIdPerFragment(WrappedOpenGL *driver, GLReplay *replay,
                                 GLPixelHistoryResources &resources,
                                 const rdcarray<EventUsage> &modEvents, int x, int y,
                                 rdcarray<PixelModification> &history,
                                 const std::map<uint32_t, uint32_t> &eventFragments,
                                 bool usingFloatForPrimitiveId, uint32_t numSamples,
                                 uint32_t sampleIndex)
{
  GLMarkerRegion region("QueryPrimitiveIdPerFragment");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);

  for(size_t i = 0; i < modEvents.size(); i++)
  {
    auto it = eventFragments.find(modEvents[i].eventId);
    uint32_t numFragments = (it != eventFragments.end()) ? it->second : 0;

    if(numFragments == 0)
    {
      if(i < modEvents.size() - 1)
      {
        driver->ReplayLog(modEvents[i].eventId, modEvents[i + 1].eventId, eReplay_WithoutDraw);
      }
      continue;
    }

    GLRenderState state;
    state.FetchState(driver);

    ResourceId ps;
    if(state.Program.name)
      ps = driver->GetProgram(driver->GetResourceManager()->GetResID(state.Program))
               .stageShaders[(int)ShaderStage::Pixel];
    else
      ps = driver->GetPipeline(driver->GetResourceManager()->GetResID(state.Pipeline))
               .stageShaders[(int)ShaderStage::Pixel];

    driver->glBindFramebuffer(eGL_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
    driver->glReadBuffer(eGL_COLOR_ATTACHMENT0);
    // rebind colour image to 0
    for(uint32_t a = 1; a < 8; a++)
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + a), 0, 0);
    driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0,
                                 resources.fullPrecisionColorImage, 0);
    driver->glDrawBuffer(eGL_COLOR_ATTACHMENT0);

    driver->glEnable(eGL_SCISSOR_TEST);
    driver->glScissor(x, y, 1, 1);

    driver->glClearStencil(0);
    driver->glEnable(eGL_DEPTH_TEST);
    if(HasExt[EXT_depth_bounds_test])
      driver->glDisable(eGL_DEPTH_BOUNDS_TEST_EXT);
    driver->glDepthFunc(eGL_ALWAYS);
    driver->glDepthMask(GL_TRUE);
    driver->glDisable(eGL_BLEND);
    driver->glDisable(eGL_SAMPLE_MASK);
    driver->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

    driver->glStencilMask(0xff);
    driver->glStencilOp(eGL_INCR, eGL_INCR, eGL_INCR);
    driver->glEnable(eGL_STENCIL_TEST);

    PixelModification referenceHistory;
    referenceHistory.eventId = modEvents[i].eventId;

    auto historyIndex =
        std::lower_bound(history.begin(), history.end(), referenceHistory,
                         [](const PixelModification &h1, const PixelModification &h2) -> bool {
                           return h1.eventId < h2.eventId;
                         });
    RDCASSERT(historyIndex != history.end());

    // we expect this value to be overwritten by the primitive id shader. if not
    // it will cause an assertion that all color values are the same to fail
    driver->glClearColor(0.84f, 0.17f, 0.2f, 0.49f);
    driver->glClear(eGL_COLOR_BUFFER_BIT);

    GLint currentProgram = 0;
    driver->glGetIntegerv(eGL_CURRENT_PROGRAM, &currentProgram);

    driver->glUseProgram(GetPrimitiveIdProgram(driver, replay, resources, currentProgram));

    driver->glBindFramebuffer(eGL_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);

    // rebind output colour to 0
    for(uint32_t a = 1; a < 8; a++)
      driver->glFramebufferTexture(eGL_FRAMEBUFFER, GLenum(eGL_COLOR_ATTACHMENT0 + a), 0, 0);
    driver->glFramebufferTexture(eGL_FRAMEBUFFER, eGL_COLOR_ATTACHMENT0,
                                 resources.fullPrecisionColorImage, 0);
    driver->glDrawBuffer(eGL_COLOR_ATTACHMENT0);
    driver->glReadBuffer(eGL_COLOR_ATTACHMENT0);

    for(size_t j = 0; j < std::max(numFragments, 1u); ++j)
    {
      ModificationValue modValue;
      modValue.stencil = 0;
      // Set the stencil function so only jth fragment will pass.
      driver->glBindFramebuffer(eGL_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
      driver->glStencilFunc(eGL_EQUAL, (int)j, 0xff);
      driver->glClear(eGL_STENCIL_BUFFER_BIT);

      driver->ReplayLog(modEvents[i].eventId, modEvents[i].eventId, eReplay_OnlyDraw);

      // Primitive id array is given size 8 for the multisampled case
      float primitiveIds[8];
      if(numSamples == 1)
      {
        ScopedReadPixelsSanitiser scope;

        driver->glReadPixels(x, y, 1, 1, eGL_RGBA, eGL_FLOAT, (void *)primitiveIds);
      }
      else
      {
        const CopyFramebuffer &copyFramebuffer = getCopyFramebuffer(
            driver, resources.copyFramebuffers, ModType::PostMod, numSamples, int(modEvents.size()),
            eGL_DEPTH32F_STENCIL8, eGL_DEPTH32F_STENCIL8, eGL_RGBA32F);
        driver->glBindFramebuffer(eGL_DRAW_FRAMEBUFFER, copyFramebuffer.framebufferId);
        driver->glBindFramebuffer(eGL_READ_FRAMEBUFFER, resources.fullPrecisionFrameBuffer);
        glReadBuffer(eGL_COLOR_ATTACHMENT0);
        SafeBlitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, getFramebufferCopyMask(driver),
                            eGL_NEAREST);
        CopyMSSample(driver, resources, copyFramebuffer, sampleIndex, 0, 0, primitiveIds);
      }

      if(primitiveIds[0] == primitiveIds[1] && primitiveIds[0] == primitiveIds[2] &&
         primitiveIds[0] == primitiveIds[3])
      {
        if(usingFloatForPrimitiveId)
        {
          historyIndex->primitiveID = int(primitiveIds[0]);
        }
        else
        {
          historyIndex->primitiveID = *(int *)primitiveIds;
        }
      }
      else
      {
        historyIndex->primitiveID = ~0u;
        RDCERR("Primitive ID was not written correctly");
      }

      if(ps == ResourceId())
        historyIndex->unboundPS = true;

      ++historyIndex;
    }
    driver->glUseProgram(currentProgram);

    state.ApplyState(driver);

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId + 1, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }
}

void CalculateFragmentDepthTests(WrappedOpenGL *driver, GLPixelHistoryResources &resources,
                                 const rdcarray<EventUsage> &modEvents,
                                 rdcarray<PixelModification> &history,
                                 const std::map<uint32_t, uint32_t> &eventFragments)
{
  GLMarkerRegion region("CalculateFragmentDepthTests");
  driver->ReplayLog(0, modEvents[0].eventId, eReplay_WithoutDraw);
  size_t historyIndex = 0;
  for(size_t i = 0; i < modEvents.size(); ++i)
  {
    // We only need to calculate the depth test for events with multiple fragments.
    auto it = eventFragments.find(modEvents[i].eventId);
    uint32_t numFragments = (it != eventFragments.end()) ? it->second : 0;
    if(numFragments <= 1)
    {
      // keep the historyIndex in sync with the event index
      while(historyIndex < history.size() && modEvents[i].eventId == history[historyIndex].eventId)
      {
        ++historyIndex;
      }

      if(i < modEvents.size() - 1)
      {
        driver->ReplayLog(modEvents[i].eventId, modEvents[i + 1].eventId, eReplay_WithoutDraw);
      }

      continue;
    }

    GLboolean depthTestEnabled = driver->glIsEnabled(eGL_DEPTH_TEST);
    // default for no depth test
    GLenum depthFunc = eGL_ALWAYS;
    if(depthTestEnabled)
      driver->glGetIntegerv(eGL_DEPTH_FUNC, (int *)&depthFunc);
    for(; historyIndex < history.size() && modEvents[i].eventId == history[historyIndex].eventId;
        ++historyIndex)
    {
      if(historyIndex == 0)
      {
        continue;
      }

      GLuint curDepth = 0;
      GLint depthType = 0;
      driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                    eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
                                                    (GLint *)&curDepth);

      driver->glGetFramebufferAttachmentParameteriv(eGL_DRAW_FRAMEBUFFER, eGL_DEPTH_ATTACHMENT,
                                                    eGL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
                                                    &depthType);

      GLenum depthFormat = eGL_NONE;

      if(curDepth != 0)
      {
        ResourceId id;
        if(depthType != eGL_RENDERBUFFER)
        {
          id = driver->GetResourceManager()->GetResID(TextureRes(driver->GetCtx(), curDepth));
        }
        else
        {
          id = driver->GetResourceManager()->GetResID(RenderbufferRes(driver->GetCtx(), curDepth));
        }
        depthFormat = driver->m_Textures[id].internalFormat;

        uint32_t depthBits = 32;
        if(depthFormat == eGL_DEPTH_COMPONENT24 || depthFormat == eGL_DEPTH24_STENCIL8 ||
           depthFormat == eGL_UNSIGNED_INT_24_8)
        {
          depthBits = 24;
        }
        else if(depthFormat == eGL_DEPTH_COMPONENT16)
        {
          depthBits = 16;
        }

        history[historyIndex].CheckDepthTestQuantised(depthBits, MakeCompareFunc(depthFunc));

        if(HasExt[EXT_depth_bounds_test] && GL.glIsEnabled(eGL_DEPTH_BOUNDS_TEST_EXT))
        {
          GLdouble depthBounds[2] = {};
          GL.glGetDoublev(eGL_DEPTH_BOUNDS_EXT, depthBounds);

          if((history[historyIndex].preMod.depth < depthBounds[0] ||
              history[historyIndex].preMod.depth > depthBounds[1]) &&
             depthBounds[1] > depthBounds[0])
          {
            history[historyIndex].depthBoundsFailed = true;
          }
        }
      }
    }

    if(i < modEvents.size() - 1)
    {
      driver->ReplayLog(modEvents[i].eventId, modEvents[i + 1].eventId, eReplay_WithoutDraw);
    }
  }
}

};    // end of anonymous namespace

rdcarray<PixelModification> GLReplay::PixelHistory(rdcarray<EventUsage> events, ResourceId target,
                                                   uint32_t x, uint32_t y, const Subresource &sub,
                                                   CompType typeCast)
{
  rdcarray<PixelModification> history;

  if(events.empty())
    return history;

  TextureDescription textureDesc = GetTexture(target);
  if(textureDesc.format.type == ResourceFormatType::Undefined)
    return history;

  // When RenderDoc passed y, the value being passed in is with the Y axis starting from the top
  // However, we need to have it starting from the bottom, so flip it by subtracting y from the
  // height.
  uint32_t flippedY = (textureDesc.height >> sub.mip) - y - 1;

  rdcstr regionName = StringFormat::Fmt(
      "PixelHistory: pixel: (%u, %u) on %s subresource (%u, %u, %u) cast to %s with %zu events", x,
      flippedY, ToStr(target).c_str(), sub.mip, sub.slice, sub.sample, ToStr(typeCast).c_str(),
      events.size());

  uint32_t sampleIdx = sub.sample;

  if(sampleIdx > textureDesc.msSamp)
  {
    sampleIdx = 0;    // if the sample index is out of range (ie. average value) then default to 0
  }

  uint32_t sampleMask = ~0U;
  if(sampleIdx < 32)
  {
    sampleMask = 1U << sampleIdx;
  }

  GLPixelHistoryResources resources;

  resources.target = target;

  MakeCurrentReplayContext(&m_ReplayCtx);

  m_pDriver->ReplayMarkers(false);

  GLMarkerRegion region(regionName);

  int glslVersion = DebugData.glslVersion;
  bool usingFloatForPrimitiveId = glslVersion < 330;

  PixelHistorySetupResources(m_pDriver, resources, textureDesc, sub, (uint32_t)events.size(),
                             glslVersion, textureDesc.msSamp);

  rdcarray<EventUsage> modEvents =
      QueryModifyingEvents(m_pDriver, resources, events, x, flippedY, sub.mip, history);

  if(modEvents.empty())
  {
    PixelHistoryDestroyResources(m_pDriver, resources);
    m_pDriver->ReplayMarkers(true);
    return history;
  }

  QueryFailedTests(m_pDriver, resources, modEvents, x, flippedY, history, sampleIdx);
  QueryPrePostModPixelValues(m_pDriver, resources, modEvents, x, flippedY, history,
                             textureDesc.msSamp, sampleIdx);

  uint32_t textureWidth = textureDesc.width >> sub.mip;
  uint32_t textureHeight = textureDesc.height >> sub.mip;

  EventFragmentData eventFragmentData =
      QueryNumFragmentsByEvent(m_pDriver, resources, modEvents, history, x, flippedY,
                               textureDesc.msSamp, sampleIdx, textureWidth, textureHeight);

  std::map<uint32_t, uint32_t> &eventFragments = eventFragmentData.eventFragments;

  // copy history entries to create one history per fragment
  for(size_t h = 0; h < history.size();)
  {
    uint32_t frags = 1;
    auto it = eventFragments.find(history[h].eventId);
    if(it != eventFragments.end())
    {
      frags = it->second;
    }
    for(uint32_t f = 1; f < frags; ++f)
    {
      history.insert(h + 1, history[h]);
    }
    for(uint32_t f = 0; f < frags; ++f)
    {
      history[h + f].fragIndex = f;
    }
    h += RDCMAX(1u, frags);
  }

  QueryPrimitiveIdPerFragment(m_pDriver, this, resources, modEvents, x, flippedY, history,
                              eventFragments, usingFloatForPrimitiveId, textureDesc.msSamp,
                              sampleIdx);
  QueryShaderOutPerFragment(m_pDriver, this, resources, modEvents, x, flippedY, history,
                            eventFragmentData, textureDesc.msSamp, sampleIdx, textureWidth,
                            textureHeight);
  QueryPrePostModPerFragment(m_pDriver, this, resources, modEvents, x, flippedY, history,
                             eventFragmentData, textureDesc.msSamp, sampleIdx, textureWidth,
                             textureHeight);

  // clear depth or colour information when not available
  for(size_t h = 0; h < history.size(); h++)
  {
    GLbitfield fbMask = eventFragmentData.eventFBMask[history[h].eventId];

    if((fbMask & GL_COLOR_BUFFER_BIT) == 0)
    {
      RDCEraseEl(history[h].preMod.col);
      RDCEraseEl(history[h].postMod.col);
    }
    if((fbMask & GL_DEPTH_BUFFER_BIT) == 0)
    {
      history[h].preMod.depth = -1;
      history[h].shaderOut.depth = -1;
      history[h].postMod.depth = -1;
    }
    if((fbMask & GL_STENCIL_BUFFER_BIT) == 0)
    {
      history[h].preMod.stencil = -1;
      history[h].shaderOut.stencil = -1;
      history[h].postMod.stencil = -1;
    }
  }

  CalculateFragmentDepthTests(m_pDriver, resources, modEvents, history, eventFragments);

  PixelHistoryDestroyResources(m_pDriver, resources);
  m_pDriver->ReplayMarkers(true);
  return history;
}
