/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-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 "replay_driver.h"
#include <float.h>
#include <math.h>
#include "compressonator/CMP_Core.h"
#include "maths/formatpacking.h"
#include "maths/half_convert.h"
#include "serialise/serialiser.h"
#include "strings/string_utils.h"

template <>
rdcstr DoStringise(const RemapTexture &el)
{
  BEGIN_ENUM_STRINGISE(RemapTexture);
  {
    STRINGISE_ENUM_CLASS(NoRemap)
    STRINGISE_ENUM_CLASS(RGBA8)
    STRINGISE_ENUM_CLASS(RGBA16)
    STRINGISE_ENUM_CLASS(RGBA32)
  }
  END_ENUM_STRINGISE();
}

template <typename SerialiserType>
void DoSerialise(SerialiserType &ser, GetTextureDataParams &el)
{
  SERIALISE_MEMBER(forDiskSave);
  SERIALISE_MEMBER(standardLayout);
  SERIALISE_MEMBER(typeCast);
  SERIALISE_MEMBER(resolve);
  SERIALISE_MEMBER(remap);
  SERIALISE_MEMBER(blackPoint);
  SERIALISE_MEMBER(whitePoint);
}

INSTANTIATE_SERIALISE_TYPE(GetTextureDataParams);

static bool PreviousNextExcludedMarker(ActionDescription *action)
{
  return bool(action->flags & (ActionFlags::PushMarker | ActionFlags::PopMarker |
                               ActionFlags::SetMarker | ActionFlags::MultiAction));
}

CompType BaseRemapType(RemapTexture remap, CompType typeCast)
{
  switch(typeCast)
  {
    case CompType::Float:
    case CompType::SNorm:
    case CompType::UNorm: return CompType::Float;
    case CompType::UNormSRGB:
      return remap == RemapTexture::RGBA8 ? CompType::UNormSRGB : CompType::Float;
    case CompType::UInt: return CompType::UInt;
    case CompType::SInt: return CompType::SInt;
    default: return typeCast;
  }
}

static ActionDescription *SetupActionPointers(rdcarray<ActionDescription *> &actionTable,
                                              rdcarray<ActionDescription> &actions,
                                              ActionDescription *parent, ActionDescription *&previous)
{
  ActionDescription *ret = NULL;

  for(size_t i = 0; i < actions.size(); i++)
  {
    ActionDescription *action = &actions[i];

    RDCASSERTMSG("All actions must have their own event as the final event",
                 !action->events.empty() && action->events.back().eventId == action->eventId);

    action->parent = parent;

    if(!action->children.empty())
    {
      {
        RDCASSERT(actionTable.empty() || action->eventId > actionTable.back()->eventId ||
                  actionTable.back()->IsFakeMarker());
        actionTable.resize(RDCMAX(actionTable.size(), size_t(action->eventId) + 1));
        actionTable[action->eventId] = action;
      }

      ret = SetupActionPointers(actionTable, action->children, action, previous);
    }
    else if(PreviousNextExcludedMarker(action))
    {
      // don't want to set up previous/next links for markers, but still add them to the table
      // Some markers like Present should have previous/next, but API Calls we also skip

      {
        // we also allow non-contiguous EIDs for fake markers that have high EIDs
        RDCASSERT(actionTable.empty() || action->eventId > actionTable.back()->eventId ||
                  actionTable.back()->IsFakeMarker());
        actionTable.resize(RDCMAX(actionTable.size(), size_t(action->eventId) + 1));
        actionTable[action->eventId] = action;
      }
    }
    else
    {
      if(previous)
        previous->next = action;
      action->previous = previous;

      {
        // we also allow non-contiguous EIDs for fake markers that have high EIDs
        RDCASSERT(actionTable.empty() || action->eventId > actionTable.back()->eventId ||
                  actionTable.back()->IsFakeMarker());
        actionTable.resize(RDCMAX(actionTable.size(), size_t(action->eventId) + 1));
        actionTable[action->eventId] = action;
      }

      ret = previous = action;
    }
  }

  return ret;
}

void SetupActionPointers(rdcarray<ActionDescription *> &actionTable,
                         rdcarray<ActionDescription> &actions)
{
  ActionDescription *previous = NULL;
  SetupActionPointers(actionTable, actions, NULL, previous);

  // markers don't enter the previous/next chain, but we still want pointers for them that point to
  // the next or previous actual action (skipping any markers). This means that
  // action->next->previous
  // != action sometimes, but it's more useful than action->next being NULL in the middle of the
  // list.
  // This enables searching for a marker string and then being able to navigate from there and
  // joining the 'real' linked list after one step.

  previous = NULL;
  rdcarray<ActionDescription *> markers;

  for(ActionDescription *action : actionTable)
  {
    if(!action)
      continue;

    bool marker = PreviousNextExcludedMarker(action);

    if(marker)
    {
      // point the previous pointer to the last non-marker action we got. If we haven't hit one yet
      // because this is near the start, this will just be NULL.
      action->previous = previous;

      // because there can be multiple markers consecutively we want to point all of their nexts to
      // the next action we encounter. Accumulate this list, though in most cases it will only be 1
      // long as it's uncommon to have multiple markers one after the other
      markers.push_back(action);
    }
    else
    {
      // the next markers we encounter should point their previous to this.
      previous = action;

      // all previous markers point to this one
      for(ActionDescription *m : markers)
        m->next = action;

      markers.clear();
    }
  }
}

void PatchLineStripIndexBuffer(const ActionDescription *action, Topology topology, uint8_t *idx8,
                               uint16_t *idx16, uint32_t *idx32, rdcarray<uint32_t> &patchedIndices)
{
  const uint32_t restart = 0xffffffff;

#define IDX_VALUE(offs)                                                                        \
  (idx16 ? idx16[index + (offs)]                                                               \
         : (idx32 ? idx32[index + (offs)] : (idx8 ? idx8[index + (offs)] : index + (offs)))) + \
      action->baseVertex

  switch(topology)
  {
    case Topology::TriangleList:
    {
      for(uint32_t index = 0; index + 3 <= action->numIndices; index += 3)
      {
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(IDX_VALUE(1));
        patchedIndices.push_back(IDX_VALUE(2));
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(restart);
      }
      break;
    }
    case Topology::TriangleStrip:
    {
      // we decompose into individual triangles. This will mean the shared lines will be overwritten
      // twice but it's a simple algorithm and otherwise decomposing a tristrip into a line strip
      // would need some more complex handling (you could two pairs of triangles in a single strip
      // by changing the winding, but then you'd need to restart and jump back, and handle a
      // trailing single triangle, etc).
      for(uint32_t index = 0; index + 3 <= action->numIndices; index++)
      {
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(IDX_VALUE(1));
        patchedIndices.push_back(IDX_VALUE(2));
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(restart);
      }
      break;
    }
    case Topology::TriangleFan:
    {
      uint32_t index = 0;
      uint32_t base = IDX_VALUE(0);
      index++;

      // this would be easier to do as a line list and just do base -> 1, 1 -> 2 lines for each
      // triangle then a base -> 2 for the last one. However I would be amazed if this code ever
      // runs except in an artificial test, so let's go with the simple and easy to understand
      // solution.
      for(; index + 2 <= action->numIndices; index++)
      {
        patchedIndices.push_back(base);
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(IDX_VALUE(1));
        patchedIndices.push_back(base);
        patchedIndices.push_back(restart);
      }
      break;
    }
    case Topology::TriangleList_Adj:
    {
      // skip the adjacency values
      for(uint32_t index = 0; index + 6 <= action->numIndices; index += 6)
      {
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(IDX_VALUE(2));
        patchedIndices.push_back(IDX_VALUE(4));
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(restart);
      }
      break;
    }
    case Topology::TriangleStrip_Adj:
    {
      // skip the adjacency values
      for(uint32_t index = 0; index + 6 <= action->numIndices; index += 2)
      {
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(IDX_VALUE(2));
        patchedIndices.push_back(IDX_VALUE(4));
        patchedIndices.push_back(IDX_VALUE(0));
        patchedIndices.push_back(restart);
      }
      break;
    }
    default:
      RDCERR("Unsupported topology %s for line-list patching", ToStr(topology).c_str());
      return;
  }

#undef IDX_VALUE
}

void PatchTriangleFanRestartIndexBufer(rdcarray<uint32_t> &patchedIndices, uint32_t restartIndex)
{
  if(patchedIndices.empty())
    return;

  rdcarray<uint32_t> newIndices;

  uint32_t firstIndex = patchedIndices[0];

  size_t i = 1;
  // while we have at least two indices left
  while(i + 1 < patchedIndices.size())
  {
    uint32_t a = patchedIndices[i];
    uint32_t b = patchedIndices[i + 1];

    if(a != restartIndex && b != restartIndex)
    {
      // no restart, add primitive
      newIndices.push_back(firstIndex);
      newIndices.push_back(a);
      newIndices.push_back(b);

      i++;
      continue;
    }
    else if(b == restartIndex)
    {
      // we've already added the last triangle before the restart in the previous iteration, just
      // continue so we hit the a == restartIndex case below
      i++;
    }
    else if(a == restartIndex)
    {
      // new first index is b
      firstIndex = b;
      // skip both the restartIndex value and the first index, and begin at the next real index (if
      // it exists)
      i += 2;

      uint32_t next[2] = {b, b};

      // if this is the last vertex, the triangle will be degenerate
      if(i < patchedIndices.size())
        next[0] = patchedIndices[i];
      if(i + 1 < patchedIndices.size())
        next[1] = patchedIndices[i + 1];

      // output 3 dummy degenerate triangles so vertex ID mapping is easy
      // we rotate the triangles so the important vertex is last in each.
      for(size_t dummy = 0; dummy < 3; dummy++)
      {
        newIndices.push_back(restartIndex);
        newIndices.push_back(restartIndex);
        newIndices.push_back(restartIndex);
      }
    }
  }

  newIndices.swap(patchedIndices);
}

void StandardFillCBufferVariable(ResourceId shader, const ShaderConstantType &desc,
                                 uint32_t dataOffset, const bytebuf &data, ShaderVariable &outvar,
                                 uint32_t matStride)
{
  const VarType type = outvar.type;
  const uint32_t rows = outvar.rows;
  const uint32_t cols = outvar.columns;

  size_t elemByteSize = 4;
  if(type == VarType::Double || type == VarType::ULong || type == VarType::SLong ||
     type == VarType::GPUPointer)
    elemByteSize = 8;
  else if(type == VarType::Half || type == VarType::UShort || type == VarType::SShort)
    elemByteSize = 2;
  else if(type == VarType::UByte || type == VarType::SByte)
    elemByteSize = 1;

  // primary is the 'major' direction
  // so a matrix is a secondaryDim number of primaryDim-sized vectors
  uint32_t primaryDim = cols;
  uint32_t secondaryDim = rows;
  if(rows > 1 && outvar.ColMajor())
  {
    primaryDim = rows;
    secondaryDim = cols;
  }

  if(dataOffset < data.size())
  {
    const byte *srcData = data.data() + dataOffset;
    const size_t avail = data.size() - dataOffset;

    byte *dstData = outvar.value.u8v.data();

    // each secondaryDim element (row or column) is stored in a primaryDim-vector.
    // We copy each vector member individually to account for smaller than uint32 sized types.
    for(uint32_t s = 0; s < secondaryDim; s++)
    {
      for(uint32_t p = 0; p < primaryDim; p++)
      {
        const size_t srcOffset = matStride * s + p * elemByteSize;
        const size_t dstOffset = (primaryDim * s + p) * elemByteSize;

        if(srcOffset + elemByteSize <= avail)
          memcpy(dstData + dstOffset, srcData + srcOffset, elemByteSize);
      }
    }

    // if it's a matrix and not row major, transpose
    if(primaryDim > 1 && secondaryDim > 1 && outvar.ColMajor())
    {
      ShaderVariable tmp = outvar;

      if(elemByteSize == 8)
      {
        for(size_t ri = 0; ri < rows; ri++)
          for(size_t ci = 0; ci < cols; ci++)
            outvar.value.u64v[ri * cols + ci] = tmp.value.u64v[ci * rows + ri];
      }
      else if(elemByteSize == 4)
      {
        for(size_t ri = 0; ri < rows; ri++)
          for(size_t ci = 0; ci < cols; ci++)
            outvar.value.u32v[ri * cols + ci] = tmp.value.u32v[ci * rows + ri];
      }
      else if(elemByteSize == 2)
      {
        for(size_t ri = 0; ri < rows; ri++)
          for(size_t ci = 0; ci < cols; ci++)
            outvar.value.u16v[ri * cols + ci] = tmp.value.u16v[ci * rows + ri];
      }
      else if(elemByteSize == 1)
      {
        for(size_t ri = 0; ri < rows; ri++)
          for(size_t ci = 0; ci < cols; ci++)
            outvar.value.u8v[ri * cols + ci] = tmp.value.u8v[ci * rows + ri];
      }
    }
  }

  if(desc.pointerTypeID != ~0U)
    outvar.SetTypedPointer(outvar.value.u64v[0], shader, desc.pointerTypeID);
}

static void StandardFillCBufferVariables(ResourceId shader, const rdcarray<ShaderConstant> &invars,
                                         rdcarray<ShaderVariable> &outvars, const bytebuf &data,
                                         uint32_t baseOffset)
{
  for(size_t v = 0; v < invars.size(); v++)
  {
    rdcstr basename = invars[v].name;

    uint8_t rows = invars[v].type.rows;
    uint8_t cols = invars[v].type.columns;
    uint32_t elems = RDCMAX(1U, invars[v].type.elements);
    const bool rowMajor = invars[v].type.RowMajor();
    const bool isArray = elems > 1;

    const uint32_t matStride = invars[v].type.matrixByteStride;

    uint32_t dataOffset = baseOffset + invars[v].byteOffset;

    if(!invars[v].type.members.empty() || (rows == 0 && cols == 0))
    {
      ShaderVariable var;
      var.name = basename;
      var.rows = var.columns = 0;
      var.type = VarType::Struct;
      if(rowMajor)
        var.flags |= ShaderVariableFlags::RowMajorMatrix;

      rdcarray<ShaderVariable> varmembers;

      if(isArray)
      {
        var.members.resize(elems);
        for(uint32_t i = 0; i < elems; i++)
        {
          ShaderVariable &vr = var.members[i];
          vr.name = StringFormat::Fmt("%s[%u]", basename.c_str(), i);
          vr.rows = vr.columns = 0;
          vr.type = VarType::Struct;
          if(rowMajor)
            vr.flags |= ShaderVariableFlags::RowMajorMatrix;

          StandardFillCBufferVariables(shader, invars[v].type.members, vr.members, data, dataOffset);

          dataOffset += invars[v].type.arrayByteStride;
        }
      }
      else
      {
        var.type = VarType::Struct;

        StandardFillCBufferVariables(shader, invars[v].type.members, var.members, data, dataOffset);
      }

      outvars.push_back(var);

      continue;
    }

    size_t outIdx = outvars.size();
    outvars.push_back({});

    {
      const VarType type = invars[v].type.baseType;

      outvars[outIdx].name = basename;
      outvars[outIdx].rows = 1;
      outvars[outIdx].type = type;
      outvars[outIdx].columns = cols;
      if(rowMajor)
        outvars[outIdx].flags |= ShaderVariableFlags::RowMajorMatrix;

      ShaderVariable &var = outvars[outIdx];

      if(!isArray)
      {
        outvars[outIdx].rows = rows;

        StandardFillCBufferVariable(shader, invars[v].type, dataOffset, data, outvars[outIdx],
                                    matStride);
      }
      else
      {
        var.name = outvars[outIdx].name;
        var.rows = 0;
        var.columns = 0;

        rdcarray<ShaderVariable> varmembers;
        varmembers.resize(elems);

        rdcstr base = outvars[outIdx].name;

        for(uint32_t e = 0; e < elems; e++)
        {
          varmembers[e].name = StringFormat::Fmt("%s[%u]", base.c_str(), e);
          varmembers[e].rows = rows;
          varmembers[e].type = type;
          varmembers[e].columns = cols;
          if(rowMajor)
            varmembers[e].flags |= ShaderVariableFlags::RowMajorMatrix;

          uint32_t rowDataOffset = dataOffset;

          dataOffset += invars[v].type.arrayByteStride;

          StandardFillCBufferVariable(shader, invars[v].type, rowDataOffset, data, varmembers[e],
                                      matStride);
        }

        var.members = varmembers;
      }
    }
  }
}

void PreprocessLineDirectives(rdcarray<ShaderSourceFile> &sourceFiles)
{
  struct SplitFile
  {
    rdcstr filename;
    rdcarray<rdcstr> lines;
    bool modified = false;
  };

  rdcarray<SplitFile> splitFiles;

  splitFiles.resize(sourceFiles.size());

  for(size_t i = 0; i < sourceFiles.size(); i++)
    splitFiles[i].filename = sourceFiles[i].filename;

  for(size_t i = 0; i < sourceFiles.size(); i++)
  {
    rdcarray<rdcstr> srclines;

    // start off writing to the corresponding output file.
    SplitFile *dstFile = &splitFiles[i];
    bool changedFile = false;

    size_t dstLine = 0;

    // skip this file if it doesn't contain #line
    if(!sourceFiles[i].contents.contains("#line"))
      continue;

    split(sourceFiles[i].contents, srclines, '\n');
    srclines.push_back("");

    // handle #line directives by inserting empty lines or erasing as necessary
    bool seenLine = false;

    for(size_t srcLine = 0; srcLine < srclines.size(); srcLine++)
    {
      if(srclines[srcLine].empty())
      {
        dstLine++;
        continue;
      }

      char *c = &srclines[srcLine][0];
      char *end = c + srclines[srcLine].size();

      while(*c == '\t' || *c == ' ' || *c == '\r')
        c++;

      if(c == end)
      {
        // blank line, just advance line counter
        dstLine++;
        continue;
      }

      if(c + 5 > end || strncmp(c, "#line", 5) != 0)
      {
        // only consider the destination file 'modfied' (and so needing a merge later) if we've seen
        // a #line statement. Otherwise we're just doing an identity copy so far. Without this, this
        // can lead to problems e.g. if there are a few statements in a file before the #line we
        // then create a truncated list of lines with only those and then start spitting the #line
        // directives into other files. We still want to have the original file as-is so don't
        // consider it modified. However if a #line directive targets the file it's already in we
        // want to be sure that we have copied any existing implicit preamble to that file so it
        // doesn't get lost when we re-merge
        if(seenLine)
          dstFile->modified = true;

        {
          // resize up to account for the current line, if necessary
          dstFile->lines.resize(RDCMAX(dstLine + 1, dstFile->lines.size()));

          // if non-empty, append this line (to allow multiple lines on the same line
          // number to be concatenated). To avoid screwing up line numbers we have to append with
          // a comment and not a newline.

          if(dstFile->lines[dstLine].empty())
            dstFile->lines[dstLine] = srclines[srcLine];
          else
            dstFile->lines[dstLine] += " /* multiple #lines overlapping */ " + srclines[srcLine];
        }

        // advance line counter
        dstLine++;

        continue;
      }

      seenLine = true;

      // we have a #line directive
      c += 5;

      if(c >= end)
      {
        // invalid #line, just advance line counter
        dstLine++;
        continue;
      }

      while(*c == '\t' || *c == ' ')
        c++;

      if(c >= end)
      {
        // invalid #line, just advance line counter
        dstLine++;
        continue;
      }

      // invalid #line, no line number. Skip/ignore and just advance line counter
      if(*c < '0' || *c > '9')
      {
        dstLine++;
        continue;
      }

      size_t newLineNum = 0;
      while(*c >= '0' && *c <= '9')
      {
        newLineNum *= 10;
        newLineNum += int((*c) - '0');
        c++;
      }

      // convert to 0-indexed line number
      if(newLineNum > 0)
        newLineNum--;

      while(*c == '\t' || *c == ' ')
        c++;

      if(*c == '"')
      {
        // filename
        c++;

        char *filename = c;

        // parse out filename
        while(*c != '"' && *c != 0)
        {
          if(*c == '\\')
          {
            // skip escaped characters
            c += 2;
          }
          else
          {
            c++;
          }
        }

        // parsed filename successfully
        if(*c == '"')
        {
          *c = 0;

          rdcstr fname = filename;
          if(fname.empty())
            fname = "unnamed_shader";

          // find the new destination file
          bool found = false;
          size_t dstFileIdx = 0;

          for(size_t f = 0; f < splitFiles.size(); f++)
          {
            if(splitFiles[f].filename == fname)
            {
              found = true;
              dstFileIdx = f;
              break;
            }
          }

          if(found)
          {
            changedFile = (dstFile != &splitFiles[dstFileIdx]);
            dstFile = &splitFiles[dstFileIdx];
          }
          else
          {
            RDCWARN("Couldn't find filename '%s' in #line directive in debug info", fname.c_str());

            // make a dummy file to write into that won't be used.
            splitFiles.push_back(SplitFile());
            splitFiles.back().filename = fname;
            splitFiles.back().modified = true;

            changedFile = true;
            dstFile = &splitFiles.back();
          }

          // set the next line number, and continue processing
          dstLine = newLineNum;

          continue;
        }
        else
        {
          // invalid #line, ignore
          RDCERR("Couldn't parse #line directive: '%s'", srclines[srcLine].c_str());
          continue;
        }
      }
      else
      {
        // No filename. Set the next line number, and continue processing
        dstLine = newLineNum;
        continue;
      }
    }
  }

  for(size_t i = 0; i < sourceFiles.size(); i++)
  {
    if(sourceFiles[i].contents.empty() || splitFiles[i].modified)
    {
      merge(splitFiles[i].lines, sourceFiles[i].contents, '\n');
    }
  }
}

void StandardFillCBufferVariables(ResourceId shader, const rdcarray<ShaderConstant> &invars,
                                  rdcarray<ShaderVariable> &outvars, const bytebuf &data)
{
  // start with offset 0
  StandardFillCBufferVariables(shader, invars, outvars, data, 0);
}

uint64_t CalcMeshOutputSize(uint64_t curSize, uint64_t requiredOutput)
{
  if(curSize == 0)
    curSize = 32 * 1024 * 1024;

  // resize exponentially up to 256MB to avoid repeated resizes
  while(curSize < requiredOutput && curSize < 0x10000000ULL)
    curSize *= 2;

  // after that, just align the required size up to 16MB and allocate that. Otherwise we can
  // vastly-overallocate at large sizes.
  if(curSize < requiredOutput)
    curSize = AlignUp(requiredOutput, (uint64_t)0x1000000ULL);

  return curSize;
}

FloatVector HighlightCache::InterpretVertex(const byte *data, uint32_t vert, const MeshDisplay &cfg,
                                            const byte *end, bool useidx, bool &valid)
{
  FloatVector ret(0.0f, 0.0f, 0.0f, 1.0f);

  if(cfg.position.format.compType == CompType::UInt ||
     cfg.position.format.compType == CompType::SInt || cfg.position.format.compCount == 4)
    ret.w = 0.0f;

  if(useidx && idxData)
  {
    if(vert >= (uint32_t)indices.size())
    {
      valid = false;
      return ret;
    }

    vert = indices[vert];

    if(cfg.position.topology != Topology::TriangleFan && cfg.position.allowRestart)
    {
      if((cfg.position.indexByteStride == 1 && vert == 0xff) ||
         (cfg.position.indexByteStride == 2 && vert == 0xffff) ||
         (cfg.position.indexByteStride == 4 && vert == 0xffffffff))
      {
        valid = false;
        return ret;
      }
    }
  }

  return HighlightCache::InterpretVertex(data, vert, cfg.position.vertexByteStride,
                                         cfg.position.format, end, valid);
}

FloatVector HighlightCache::InterpretVertex(const byte *data, uint32_t vert,
                                            uint32_t vertexByteStride, const ResourceFormat &fmt,
                                            const byte *end, bool &valid)
{
  data += vert * vertexByteStride;

  if(data + fmt.ElementSize() > end)
  {
    valid = false;

    if(fmt.compType == CompType::UInt || fmt.compType == CompType::SInt || fmt.compCount == 4)
      return FloatVector(0.0f, 0.0f, 0.0f, 0.0f);

    return FloatVector(0.0f, 0.0f, 0.0f, 1.0f);
  }

  return DecodeFormattedComponents(fmt, data);
}

uint64_t inthash(uint64_t val, uint64_t seed)
{
  return (seed << 5) + seed + val; /* hash * 33 + c */
}

uint64_t inthash(ResourceId id, uint64_t seed)
{
  uint64_t val = 0;
  memcpy(&val, &id, sizeof(val));
  return (seed << 5) + seed + val; /* hash * 33 + c */
}

void HighlightCache::CacheHighlightingData(uint32_t eventId, const MeshDisplay &cfg)
{
  rdcstr ident;

  uint64_t newKey = 5381;

  // hash all the properties of cfg that we use
  newKey = inthash(eventId, newKey);
  newKey = inthash(cfg.position.indexByteStride, newKey);
  newKey = inthash(cfg.position.numIndices, newKey);
  newKey = inthash((uint64_t)cfg.type, newKey);
  newKey = inthash((uint64_t)cfg.position.baseVertex, newKey);
  newKey = inthash((uint64_t)cfg.position.topology, newKey);
  newKey = inthash(cfg.position.vertexByteOffset, newKey);
  newKey = inthash(cfg.position.vertexByteStride, newKey);
  newKey = inthash(cfg.position.indexResourceId, newKey);
  newKey = inthash(cfg.position.vertexResourceId, newKey);
  newKey = inthash((uint64_t)cfg.position.allowRestart, newKey);
  newKey = inthash((uint64_t)cfg.position.restartIndex, newKey);

  if(cacheKey != newKey)
  {
    cacheKey = newKey;

    uint32_t bytesize = cfg.position.indexByteStride;
    uint64_t maxIndex = cfg.position.numIndices - 1;

    if(cfg.position.indexByteStride == 0 || cfg.type == MeshDataStage::GSOut)
    {
      indices.clear();
      idxData = false;
    }
    else
    {
      idxData = true;

      bytebuf idxdata;
      if(cfg.position.indexResourceId != ResourceId())
        driver->GetBufferData(cfg.position.indexResourceId, cfg.position.indexByteOffset,
                              cfg.position.numIndices * bytesize, idxdata);

      uint8_t *idx8 = (uint8_t *)&idxdata[0];
      uint16_t *idx16 = (uint16_t *)&idxdata[0];
      uint32_t *idx32 = (uint32_t *)&idxdata[0];

      uint32_t numIndices = RDCMIN(cfg.position.numIndices, uint32_t(idxdata.size() / bytesize));

      indices.resize(numIndices);

      if(bytesize == 1)
      {
        for(uint32_t i = 0; i < numIndices; i++)
        {
          indices[i] = uint32_t(idx8[i]);
          maxIndex = RDCMAX(maxIndex, (uint64_t)indices[i]);
        }
      }
      else if(bytesize == 2)
      {
        for(uint32_t i = 0; i < numIndices; i++)
        {
          indices[i] = uint32_t(idx16[i]);
          maxIndex = RDCMAX(maxIndex, (uint64_t)indices[i]);
        }
      }
      else if(bytesize == 4)
      {
        for(uint32_t i = 0; i < numIndices; i++)
        {
          indices[i] = idx32[i];
          maxIndex = RDCMAX(maxIndex, (uint64_t)indices[i]);
        }
      }

      uint32_t sub = uint32_t(-cfg.position.baseVertex);
      uint32_t add = uint32_t(cfg.position.baseVertex);

      if(cfg.position.baseVertex > 0)
        maxIndex += add;

      uint32_t primRestart = 0;
      if(cfg.position.allowRestart)
      {
        if(cfg.position.indexByteStride == 1)
          primRestart = 0xff;
        else if(cfg.position.indexByteStride == 2)
          primRestart = 0xffff;
        else
          primRestart = 0xffffffff;
      }

      for(uint32_t i = 0; cfg.position.baseVertex != 0 && i < numIndices; i++)
      {
        // don't modify primitive restart indices
        if(primRestart && indices[i] == primRestart)
          continue;

        if(cfg.position.baseVertex < 0)
        {
          if(indices[i] < sub)
            indices[i] = 0;
          else
            indices[i] -= sub;
        }
        else
        {
          indices[i] += add;
        }
      }
    }

    driver->GetBufferData(cfg.position.vertexResourceId, cfg.position.vertexByteOffset,
                          (maxIndex + 1) * cfg.position.vertexByteStride, vertexData);

    // if it's a fan AND it uses primitive restart, decompose it into a triangle list because the
    // restart changes the central vertex
    if(cfg.position.topology == Topology::TriangleFan && cfg.position.allowRestart)
    {
      PatchTriangleFanRestartIndexBufer(indices, cfg.position.restartIndex);
    }
  }
}

bool HighlightCache::FetchHighlightPositions(const MeshDisplay &cfg, FloatVector &activeVertex,
                                             rdcarray<FloatVector> &activePrim,
                                             rdcarray<FloatVector> &adjacentPrimVertices,
                                             rdcarray<FloatVector> &inactiveVertices)
{
  bool valid = true;

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

  uint32_t idx = cfg.highlightVert;
  Topology meshtopo = cfg.position.topology;

  // if it's a fan AND it uses primitive restart, it was decomposed into a triangle list
  if(meshtopo == Topology::TriangleFan && cfg.position.allowRestart)
  {
    meshtopo = Topology::TriangleList;

    // due to triangle fan expansion the index we need to look up is adjusted
    if(idx > 2)
      idx = (idx - 1) * 3 - 1;
  }

  activeVertex = InterpretVertex(data, idx, cfg, dataEnd, true, valid);

  uint32_t primRestart = 0;
  if(cfg.position.allowRestart)
  {
    if(cfg.position.indexByteStride == 1)
      primRestart = 0xff;
    else if(cfg.position.indexByteStride == 2)
      primRestart = 0xffff;
    else
      primRestart = 0xffffffff;
  }

  // Reference for how primitive topologies are laid out:
  // http://msdn.microsoft.com/en-us/library/windows/desktop/bb205124(v=vs.85).aspx
  // Section 19.1 of the Vulkan 1.0.48 spec
  // Section 10.1 of the OpenGL 4.5 spec
  if(meshtopo == Topology::LineList)
  {
    uint32_t v = uint32_t(idx / 2) * 2;    // find first vert in primitive

    activePrim.push_back(InterpretVertex(data, v + 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 1, cfg, dataEnd, true, valid));
  }
  else if(meshtopo == Topology::TriangleList)
  {
    uint32_t v = uint32_t(idx / 3) * 3;    // find first vert in primitive

    activePrim.push_back(InterpretVertex(data, v + 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 1, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 2, cfg, dataEnd, true, valid));
  }
  else if(meshtopo == Topology::LineList_Adj)
  {
    uint32_t v = uint32_t(idx / 4) * 4;    // find first vert in primitive

    FloatVector vs[] = {
        InterpretVertex(data, v + 0, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 1, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 2, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 3, cfg, dataEnd, true, valid),
    };

    adjacentPrimVertices.push_back(vs[0]);
    adjacentPrimVertices.push_back(vs[1]);

    adjacentPrimVertices.push_back(vs[2]);
    adjacentPrimVertices.push_back(vs[3]);

    activePrim.push_back(vs[1]);
    activePrim.push_back(vs[2]);
  }
  else if(meshtopo == Topology::TriangleList_Adj)
  {
    uint32_t v = uint32_t(idx / 6) * 6;    // find first vert in primitive

    FloatVector vs[] = {
        InterpretVertex(data, v + 0, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 1, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 2, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 3, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 4, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 5, cfg, dataEnd, true, valid),
    };

    adjacentPrimVertices.push_back(vs[0]);
    adjacentPrimVertices.push_back(vs[1]);
    adjacentPrimVertices.push_back(vs[2]);

    adjacentPrimVertices.push_back(vs[2]);
    adjacentPrimVertices.push_back(vs[3]);
    adjacentPrimVertices.push_back(vs[4]);

    adjacentPrimVertices.push_back(vs[4]);
    adjacentPrimVertices.push_back(vs[5]);
    adjacentPrimVertices.push_back(vs[0]);

    activePrim.push_back(vs[0]);
    activePrim.push_back(vs[2]);
    activePrim.push_back(vs[4]);
  }
  else if(meshtopo == Topology::LineStrip)
  {
    // find first vert in primitive. In strips a vert isn't
    // in only one primitive, so we pick the first primitive
    // it's in. This means the first N points are in the first
    // primitive, and thereafter each point is in the next primitive
    uint32_t v = RDCMAX(idx, 1U) - 1;

    // skip past any primitive restart indices
    if(idxData && primRestart)
    {
      while(v < (uint32_t)indices.size() && indices[v] == primRestart)
        v++;
    }

    activePrim.push_back(InterpretVertex(data, v + 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 1, cfg, dataEnd, true, valid));
  }
  else if(meshtopo == Topology::TriangleFan)
  {
    // find first vert in primitive. In fans a vert isn't
    // in only one primitive, so we pick the first primitive
    // it's in. This means the first N points are in the first
    // primitive, and thereafter each point is in the next primitive
    uint32_t v = RDCMAX(idx, 2U) - 1;

    // first vert in the whole fan
    activePrim.push_back(InterpretVertex(data, 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 1, cfg, dataEnd, true, valid));
  }
  else if(meshtopo == Topology::TriangleStrip)
  {
    // find first vert in primitive. In strips a vert isn't
    // in only one primitive, so we pick the first primitive
    // it's in. This means the first N points are in the first
    // primitive, and thereafter each point is in the next primitive
    uint32_t v = RDCMAX(idx, 2U) - 2;

    // skip past any primitive restart indices
    if(idxData && primRestart)
    {
      while(v < (uint32_t)indices.size() &&
            (indices[v + 0] == primRestart || indices[v + 1] == primRestart))
        v++;
    }

    activePrim.push_back(InterpretVertex(data, v + 0, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 1, cfg, dataEnd, true, valid));
    activePrim.push_back(InterpretVertex(data, v + 2, cfg, dataEnd, true, valid));
  }
  else if(meshtopo == Topology::LineStrip_Adj)
  {
    // find first vert in primitive. In strips a vert isn't
    // in only one primitive, so we pick the first primitive
    // it's in. This means the first N points are in the first
    // primitive, and thereafter each point is in the next primitive
    uint32_t v = RDCMAX(idx, 3U) - 3;

    // skip past any primitive restart indices
    if(idxData && primRestart)
    {
      while(v < (uint32_t)indices.size() &&
            (indices[v + 0] == primRestart || indices[v + 1] == primRestart ||
             indices[v + 2] == primRestart))
        v++;
    }

    FloatVector vs[] = {
        InterpretVertex(data, v + 0, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 1, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 2, cfg, dataEnd, true, valid),
        InterpretVertex(data, v + 3, cfg, dataEnd, true, valid),
    };

    adjacentPrimVertices.push_back(vs[0]);
    adjacentPrimVertices.push_back(vs[1]);

    adjacentPrimVertices.push_back(vs[2]);
    adjacentPrimVertices.push_back(vs[3]);

    activePrim.push_back(vs[1]);
    activePrim.push_back(vs[2]);
  }
  else if(meshtopo == Topology::TriangleStrip_Adj)
  {
    // Triangle strip with adjacency is the most complex topology, as
    // we need to handle the ends separately where the pattern breaks.

    uint32_t numidx = cfg.position.numIndices;

    if(numidx < 6)
    {
      // not enough indices provided, bail to make sure logic below doesn't
      // need to have tons of edge case detection
      valid = false;
    }
    else if(idx <= 4 || numidx <= 7)
    {
      FloatVector vs[] = {
          InterpretVertex(data, 0, cfg, dataEnd, true, valid),
          InterpretVertex(data, 1, cfg, dataEnd, true, valid),
          InterpretVertex(data, 2, cfg, dataEnd, true, valid),
          InterpretVertex(data, 3, cfg, dataEnd, true, valid),
          InterpretVertex(data, 4, cfg, dataEnd, true, valid),

          // note this one isn't used as it's adjacency for the next triangle
          InterpretVertex(data, 5, cfg, dataEnd, true, valid),

          // min() with number of indices in case this is a tiny strip
          // that is basically just a list
          InterpretVertex(data, RDCMIN(6U, numidx - 1), cfg, dataEnd, true, valid),
      };

      // these are the triangles on the far left of the MSDN diagram above
      adjacentPrimVertices.push_back(vs[0]);
      adjacentPrimVertices.push_back(vs[1]);
      adjacentPrimVertices.push_back(vs[2]);

      adjacentPrimVertices.push_back(vs[4]);
      adjacentPrimVertices.push_back(vs[3]);
      adjacentPrimVertices.push_back(vs[0]);

      adjacentPrimVertices.push_back(vs[4]);
      adjacentPrimVertices.push_back(vs[2]);
      adjacentPrimVertices.push_back(vs[6]);

      activePrim.push_back(vs[0]);
      activePrim.push_back(vs[2]);
      activePrim.push_back(vs[4]);
    }
    else if(idx > numidx - 4)
    {
      // in diagram, numidx == 14

      FloatVector vs[] = {
          /*[0]=*/InterpretVertex(data, numidx - 8, cfg, dataEnd, true, valid),    // 6 in diagram

          // as above, unused since this is adjacency for 2-previous triangle
          /*[1]=*/InterpretVertex(data, numidx - 7, cfg, dataEnd, true, valid),    // 7 in diagram
          /*[2]=*/InterpretVertex(data, numidx - 6, cfg, dataEnd, true, valid),    // 8 in diagram

          // as above, unused since this is adjacency for previous triangle
          /*[3]=*/InterpretVertex(data, numidx - 5, cfg, dataEnd, true, valid),    // 9 in diagram

          /*[4]=*/    // 10 in diagram
          InterpretVertex(data, numidx - 4, cfg, dataEnd, true, valid),

          /*[5]=*/    // 11 in diagram
          InterpretVertex(data, numidx - 3, cfg, dataEnd, true, valid),

          /*[6]=*/    // 12 in diagram
          InterpretVertex(data, numidx - 2, cfg, dataEnd, true, valid),

          /*[7]=*/    // 13 in diagram
          InterpretVertex(data, numidx - 1, cfg, dataEnd, true, valid),
      };

      // these are the triangles on the far right of the MSDN diagram above
      adjacentPrimVertices.push_back(vs[2]);    // 8 in diagram
      adjacentPrimVertices.push_back(vs[0]);    // 6 in diagram
      adjacentPrimVertices.push_back(vs[4]);    // 10 in diagram

      adjacentPrimVertices.push_back(vs[4]);    // 10 in diagram
      adjacentPrimVertices.push_back(vs[7]);    // 13 in diagram
      adjacentPrimVertices.push_back(vs[6]);    // 12 in diagram

      adjacentPrimVertices.push_back(vs[6]);    // 12 in diagram
      adjacentPrimVertices.push_back(vs[5]);    // 11 in diagram
      adjacentPrimVertices.push_back(vs[2]);    // 8 in diagram

      activePrim.push_back(vs[2]);    // 8 in diagram
      activePrim.push_back(vs[4]);    // 10 in diagram
      activePrim.push_back(vs[6]);    // 12 in diagram
    }
    else
    {
      // we're in the middle somewhere. Each primitive has two vertices for it
      // so our step rate is 2. The first 'middle' primitive starts at indices 5&6
      // and uses indices all the way back to 0
      uint32_t v = RDCMAX(((idx + 1) / 2) * 2, 6U) - 6;

      // skip past any primitive restart indices
      if(idxData && primRestart)
      {
        while(v < (uint32_t)indices.size() &&
              (indices[v + 0] == primRestart || indices[v + 1] == primRestart ||
               indices[v + 2] == primRestart || indices[v + 3] == primRestart ||
               indices[v + 4] == primRestart || indices[v + 5] == primRestart))
          v++;
      }

      // these correspond to the indices in the MSDN diagram, with {2,4,6} as the
      // main triangle
      FloatVector vs[] = {
          InterpretVertex(data, v + 0, cfg, dataEnd, true, valid),

          // this one is adjacency for 2-previous triangle
          InterpretVertex(data, v + 1, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 2, cfg, dataEnd, true, valid),

          // this one is adjacency for previous triangle
          InterpretVertex(data, v + 3, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 4, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 5, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 6, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 7, cfg, dataEnd, true, valid),
          InterpretVertex(data, v + 8, cfg, dataEnd, true, valid),
      };

      // these are the triangles around {2,4,6} in the MSDN diagram above
      adjacentPrimVertices.push_back(vs[0]);
      adjacentPrimVertices.push_back(vs[2]);
      adjacentPrimVertices.push_back(vs[4]);

      adjacentPrimVertices.push_back(vs[2]);
      adjacentPrimVertices.push_back(vs[5]);
      adjacentPrimVertices.push_back(vs[6]);

      adjacentPrimVertices.push_back(vs[6]);
      adjacentPrimVertices.push_back(vs[8]);
      adjacentPrimVertices.push_back(vs[4]);

      activePrim.push_back(vs[2]);
      activePrim.push_back(vs[4]);
      activePrim.push_back(vs[6]);
    }
  }
  else if(meshtopo >= Topology::PatchList)
  {
    uint32_t dim = PatchList_Count(meshtopo);

    uint32_t v0 = uint32_t(idx / dim) * dim;

    for(uint32_t v = v0; v < v0 + dim; v++)
    {
      if(v != idx && valid)
        inactiveVertices.push_back(InterpretVertex(data, v, cfg, dataEnd, true, valid));
    }
  }
  else    // if(meshtopo == Topology::PointList) point list, or unknown/unhandled type
  {
    // no adjacency, inactive verts or active primitive
  }

  return valid;
}

// colour ramp from http://www.ncl.ucar.edu/Document/Graphics/ColorTables/GMT_wysiwyg.shtml
const Vec4f colorRamp[22] = {
    Vec4f(0.000000f, 0.000000f, 0.000000f, 0.0f), Vec4f(0.250980f, 0.000000f, 0.250980f, 1.0f),
    Vec4f(0.250980f, 0.000000f, 0.752941f, 1.0f), Vec4f(0.000000f, 0.250980f, 1.000000f, 1.0f),
    Vec4f(0.000000f, 0.501961f, 1.000000f, 1.0f), Vec4f(0.000000f, 0.627451f, 1.000000f, 1.0f),
    Vec4f(0.250980f, 0.752941f, 1.000000f, 1.0f), Vec4f(0.250980f, 0.878431f, 1.000000f, 1.0f),
    Vec4f(0.250980f, 1.000000f, 1.000000f, 1.0f), Vec4f(0.250980f, 1.000000f, 0.752941f, 1.0f),
    Vec4f(0.250980f, 1.000000f, 0.250980f, 1.0f), Vec4f(0.501961f, 1.000000f, 0.250980f, 1.0f),
    Vec4f(0.752941f, 1.000000f, 0.250980f, 1.0f), Vec4f(1.000000f, 1.000000f, 0.250980f, 1.0f),
    Vec4f(1.000000f, 0.878431f, 0.250980f, 1.0f), Vec4f(1.000000f, 0.627451f, 0.250980f, 1.0f),
    Vec4f(1.000000f, 0.376471f, 0.250980f, 1.0f), Vec4f(1.000000f, 0.125490f, 0.250980f, 1.0f),
    Vec4f(1.000000f, 0.376471f, 0.752941f, 1.0f), Vec4f(1.000000f, 0.627451f, 1.000000f, 1.0f),
    Vec4f(1.000000f, 0.878431f, 1.000000f, 1.0f), Vec4f(1.000000f, 1.000000f, 1.000000f, 1.0f),
};

// unique colors generated from https://mokole.com/palette.html
const uint32_t uniqueColors[48] = {
    0xff00008b, 0xff32cd32, 0xff8fbc8f, 0xff8b008b, 0xffb03060, 0xffd2b48c, 0xff9932cc, 0xffff0000,
    0xffff8c00, 0xffffd700, 0xff00ff00, 0xff00ff7f, 0xff4169e1, 0xffe9967a, 0xffdc143c, 0xff00ffff,
    0xff00bfff, 0xff0000ff, 0xffa020f0, 0xffadff2f, 0xffff6347, 0xffda70d6, 0xffff00ff, 0xfff0e68c,
    0xffffff54, 0xff6495ed, 0xffdda0dd, 0xff90ee90, 0xff87ceeb, 0xffff1493, 0xff7fffd4, 0xffff69b4,
    0xff808080, 0xffc0c0c0, 0xff2f4f4f, 0xff556b2f, 0xff8b4513, 0xff6b8e23, 0xff2e8b57, 0xff8b0000,
    0xff483d8b, 0xff008000, 0xffb8860b, 0xff008b8b, 0xff4682b4, 0xffd2691e, 0xff9acd32, 0xffcd5c5c,

};

bytebuf GetDiscardPattern(DiscardType type, const ResourceFormat &fmt, uint32_t rowPitch, bool invert)
{
  static const rdcliteral patterns[] = {
      // DiscardType::RenderPassLoad
      "..#.....##...##..##....##....##..#..#.####..###..##..###..####.."
      "..#....#..#.#..#.#.#...#.#..#..#.##.#..#...#....#..#.#..#.#....."
      "..#....#..#.#..#.#..#..#..#.#..#.##.#..#...#....#..#.#..#.###..."
      "..#....#..#.####.#..#..#..#.#..#.#.##..#...#....####.###..#....."
      "..#....#..#.#..#.#.#...#.#..#..#.#.##..#...#....#..#.#..#.#....."
      "..####..##..#..#.##....##....##..#..#..#....###.#..#.#..#.####.."
      "................................................................"
      "................................................................"_lit,

      // DiscardType::RenderPassStore
      "...###.####..##..###...##....##..#..#.####..###..##..###..####.."
      "..#.....#...#..#.#..#..#.#..#..#.##.#..#...#....#..#.#..#.#....."
      "...#....#...#..#.#..#..#..#.#..#.##.#..#...#....#..#.#..#.###..."
      "....#...#...#..#.###...#..#.#..#.#.##..#...#....####.###..#....."
      ".....#..#...#..#.#..#..#.#..#..#.#.##..#...#....#..#.#..#.#....."
      "..###...#....##..#..#..##....##..#..#..#....###.#..#.#..#.####.."
      "................................................................"
      "................................................................"_lit,

      // DiscardType::UndefinedTransition
      "..#..#.#..#.##...####.####.####.#..#.####.##....####.#..#..###.."
      "..#..#.##.#.#.#..#....#.....#...##.#.#....#.#....#...####.#....."
      "..#..#.##.#.#..#.###..###...#...##.#.###..#..#...#...##.#.#....."
      "..#..#.#.##.#..#.#....#.....#...#.##.#....#..#...#...#..#.#.##.."
      "..#..#.#.##.#.#..#....#.....#...#.##.#....#.#....#...#..#.#..#.."
      "...##..#..#.##...####.#....####.#..#.####.##....####.#..#..##..."
      "................................................................"
      "................................................................"_lit,

      // DiscardType::DiscardCall
      "..##...####..###..###...#...###..##...####.##...####.#..#..###.."
      "..#.#...#...#....#.....#.#..#..#.#.#..#....#.#...#...####.#....."
      "..#..#..#....#...#.....#.#..#..#.#..#.###..#..#..#...##.#.#....."
      "..#..#..#.....#..#....#####.###..#..#.#....#..#..#...#..#.#.##.."
      "..#.#...#......#.#....#...#.#..#.#.#..#....#.#...#...#..#.#..#.."
      "..##...####.###...###.#...#.#..#.##...####.##...####.#..#..##..."
      "................................................................"
      "................................................................"_lit,
      // DiscardType::InvalidateCall
      "...####.#..#.#...#...#...#....####.##.....#...#####.####.##....."
      "....#...##.#.#...#..#.#..#.....#...#.#...#.#....#...#....#.#...."
      "....#...##.#.#...#..#.#..#.....#...#..#..#.#....#...###..#..#..."
      "....#...#.##..#.#..#####.#.....#...#..#.#####...#...#....#..#..."
      "....#...#.##..#.#..#...#.#.....#...#.#..#...#...#...#....#.#...."
      "...####.#..#...#...#...#.####.####.##...#...#...#...####.##....."
      "................................................................"
      "................................................................"_lit,
  };

  const rdcliteral &pattern = patterns[(int)type];

  RDCASSERT(pattern.length() == DiscardPatternWidth * DiscardPatternHeight);

  bytebuf ret;

  if(fmt.type == ResourceFormatType::Regular || fmt.type == ResourceFormatType::A8 ||
     fmt.type == ResourceFormatType::S8)
  {
    byte black[8] = {};
    byte white[8] = {};

    if(fmt.compType == CompType::Float)
    {
      if(fmt.compByteWidth == 8)
      {
        double b = 0.0;
        double w = 1000.0;
        memcpy(black, &b, sizeof(b));
        memcpy(white, &w, sizeof(w));
      }
      else if(fmt.compByteWidth == 4)
      {
        float b = 0.0f;
        float w = 1000.0f;
        memcpy(black, &b, sizeof(b));
        memcpy(white, &w, sizeof(w));
      }
      else
      {
        uint16_t b = ConvertToHalf(0.0f);
        uint16_t w = ConvertToHalf(1000.0f);
        memcpy(black, &b, sizeof(b));
        memcpy(white, &w, sizeof(w));
      }
    }
    else if(fmt.compType == CompType::Depth)
    {
      if(fmt.compByteWidth == 4)
      {
        float b = 0.0f;
        float w = 1.0f;
        memcpy(black, &b, sizeof(b));
        memcpy(white, &w, sizeof(w));
      }
      else
      {
        // other depth formats are normalised
        memset(black, 0, sizeof(black));
        memset(white, 0xff, sizeof(white));
      }
    }
    else if(fmt.compType == CompType::UInt || fmt.compType == CompType::SInt)
    {
      // ints we use 0 and 127 so it's the same for every signed type and byte width
      white[0] = 127;
    }
    else
    {
      // all other types are normalised, so we just set white to 0xff
      memset(black, 0, sizeof(black));
      memset(white, 0xff, sizeof(white));
    }

    uint32_t tightPitch = DiscardPatternWidth * fmt.compByteWidth * fmt.compCount;
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    byte *out = ret.data();

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        for(uint8_t i = 0; i < fmt.compCount; i++)
        {
          if(c == '#')
            memcpy(out, white, fmt.compByteWidth);
          else
            memcpy(out, black, fmt.compByteWidth);
          out += fmt.compByteWidth;
        }
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::R10G10B10A2)
  {
    uint32_t tightPitch = DiscardPatternWidth * sizeof(uint32_t);
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    uint32_t *out = (uint32_t *)ret.data();

    uint32_t minVal = 0;
    uint32_t maxVal = 0xffffffff;

    if(fmt.compType == CompType::UInt)
      maxVal = (127u << 0) | (127u << 10) | (127u << 20) | (3u << 30);

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        *(out++) = (c == '#') ? maxVal : minVal;
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::R5G6B5 || fmt.type == ResourceFormatType::R5G5B5A1 ||
          fmt.type == ResourceFormatType::R4G4B4A4)
  {
    uint32_t tightPitch = DiscardPatternWidth * sizeof(uint16_t);
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    uint16_t *out = (uint16_t *)ret.data();

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        *(out++) = (c == '#') ? 0xffff : 0x0000;
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::R4G4)
  {
    uint32_t tightPitch = DiscardPatternWidth * sizeof(uint8_t);
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    byte *out = ret.data();

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        *(out++) = (c == '#') ? 0xff : 0x00;
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::R11G11B10)
  {
    const uint32_t black = ConvertToR11G11B10(Vec3f(0.0f, 0.0f, 0.0f));
    const uint32_t white = ConvertToR11G11B10(Vec3f(1000.0f, 1000.0f, 1000.0f));

    uint32_t tightPitch = DiscardPatternWidth * sizeof(uint32_t);
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    uint32_t *out = (uint32_t *)ret.data();

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        *(out++) = (c == '#') ? white : black;
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::R9G9B9E5)
  {
    const uint32_t black = ConvertToR9G9B9E5(Vec3f(0.0f, 0.0f, 0.0f));
    const uint32_t white = ConvertToR9G9B9E5(Vec3f(1000.0f, 1000.0f, 1000.0f));

    uint32_t tightPitch = DiscardPatternWidth * sizeof(uint32_t);
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);
    uint32_t *out = (uint32_t *)ret.data();

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        *(out++) = (c == '#') ? white : black;
      }
      out += (rowPitch - tightPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::D16S8 || fmt.type == ResourceFormatType::D24S8 ||
          fmt.type == ResourceFormatType::D32S8)
  {
    uint32_t white = 0xffffffff;
    uint32_t black = 0;

    uint32_t depthStride = 0;

    if(fmt.type == ResourceFormatType::D16S8)
    {
      depthStride = 2;
    }
    else if(fmt.type == ResourceFormatType::D24S8)
    {
      depthStride = 4;
    }
    else if(fmt.type == ResourceFormatType::D32S8)
    {
      depthStride = 4;
      float maxDepth = 1.0f;
      memcpy(&white, &maxDepth, sizeof(float));
    }

    uint32_t tightPitch = DiscardPatternWidth * depthStride;
    uint32_t tightStencilPitch = DiscardPatternWidth * sizeof(byte);
    rowPitch = RDCMAX(rowPitch, RDCMAX(tightPitch, tightStencilPitch));

    ret.resize(rowPitch * DiscardPatternHeight * 2);
    byte *depthOut = ret.data();
    byte *stencilOut = depthOut + rowPitch * DiscardPatternHeight;

    for(int yi = 0; yi < (int)DiscardPatternHeight; yi++)
    {
      int y = invert ? DiscardPatternHeight - 1 - yi : yi;
      for(int x = 0; x < (int)DiscardPatternWidth; x++)
      {
        char c = pattern.c_str()[y * DiscardPatternWidth + x];
        if(c == '#')
        {
          memcpy(depthOut, &white, depthStride);
          *(stencilOut++) = 0xff;
        }
        else
        {
          memcpy(depthOut, &black, depthStride);
          *(stencilOut++) = 0x00;
        }

        depthOut += depthStride;
      }

      depthOut += (rowPitch - tightPitch);
      stencilOut += (rowPitch - tightStencilPitch);
    }
  }
  else if(fmt.type == ResourceFormatType::BC1 || fmt.type == ResourceFormatType::BC2 ||
          fmt.type == ResourceFormatType::BC3 || fmt.type == ResourceFormatType::BC4 ||
          fmt.type == ResourceFormatType::BC5 || fmt.type == ResourceFormatType::BC6 ||
          fmt.type == ResourceFormatType::BC7)
  {
#if ENABLED(RDOC_ANDROID)
    RDCERR("Format %s not supported on android", fmt.Name().c_str());
#else
    const uint16_t whalf = ConvertToHalf(1000.0f);

    byte block[16];

    uint32_t blockSize =
        fmt.type == ResourceFormatType::BC1 || fmt.type == ResourceFormatType::BC4 ? 8 : 16;
    uint32_t tightPitch = (DiscardPatternWidth / 4) * blockSize;
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.reserve(rowPitch * (DiscardPatternHeight / 4));

    bytebuf inblock;

    void *bc6opts = NULL;

    CreateOptionsBC6(&bc6opts);

    SetQualityBC6(bc6opts, 0.1f);

    for(uint32_t yi = 0; yi < DiscardPatternHeight; yi += 4)
    {
      uint32_t baseY = invert ? DiscardPatternHeight - 1 - yi : yi;

      for(uint32_t baseX = 0; baseX < DiscardPatternWidth; baseX += 4)
      {
        inblock.clear();

        // inblock is 4x4 RGBA8_UNORM
        if(fmt.type == ResourceFormatType::BC1 || fmt.type == ResourceFormatType::BC2 ||
           fmt.type == ResourceFormatType::BC3 || fmt.type == ResourceFormatType::BC7)
        {
          for(uint32_t y = baseY; y < baseY + 4; invert ? y-- : y++)
          {
            for(uint32_t x = baseX; x < baseX + 4; x++)
            {
              char c = pattern.c_str()[y * DiscardPatternWidth + x];

              inblock.push_back(c == '#' ? 0xff : 0x00);
              inblock.push_back(c == '#' ? 0xff : 0x00);
              inblock.push_back(c == '#' ? 0xff : 0x00);
              inblock.push_back(c == '#' ? 0xff : 0x00);
            }
          }
        }
        // inblock is 4x4 R8_UNORM
        else if(fmt.type == ResourceFormatType::BC4 || fmt.type == ResourceFormatType::BC5)
        {
          for(uint32_t y = baseY; y < baseY + 4; invert ? y-- : y++)
          {
            for(uint32_t x = baseX; x < baseX + 4; x++)
            {
              char c = pattern.c_str()[y * DiscardPatternWidth + x];

              inblock.push_back(c == '#' ? 0xff : 0x00);
            }
          }
        }
        // inblock is 4x4 RGB16_FLOAT
        else if(fmt.type == ResourceFormatType::BC6)
        {
          for(uint32_t y = baseY; y < baseY + 4; invert ? y-- : y++)
          {
            for(uint32_t x = baseX; x < baseX + 4; x++)
            {
              char c = pattern.c_str()[y * DiscardPatternWidth + x];

              inblock.push_back(c == '#' ? (whalf & 0xff) : 0x00);
              inblock.push_back(c == '#' ? ((whalf >> 8) & 0xff) : 0x00);

              inblock.push_back(c == '#' ? (whalf & 0xff) : 0x00);
              inblock.push_back(c == '#' ? ((whalf >> 8) & 0xff) : 0x00);

              inblock.push_back(c == '#' ? (whalf & 0xff) : 0x00);
              inblock.push_back(c == '#' ? ((whalf >> 8) & 0xff) : 0x00);
            }
          }
        }

        if(fmt.type == ResourceFormatType::BC1)
          CompressBlockBC1(inblock.data(), 4 * sizeof(uint32_t), block, NULL);
        else if(fmt.type == ResourceFormatType::BC2)
          CompressBlockBC2(inblock.data(), 4 * sizeof(uint32_t), block, NULL);
        else if(fmt.type == ResourceFormatType::BC3)
          CompressBlockBC3(inblock.data(), 4 * sizeof(uint32_t), block, NULL);
        else if(fmt.type == ResourceFormatType::BC4)
          CompressBlockBC4(inblock.data(), 4, block, NULL);
        else if(fmt.type == ResourceFormatType::BC5)
          CompressBlockBC5(inblock.data(), 4, inblock.data(), 4, block, NULL);
        else if(fmt.type == ResourceFormatType::BC6)
          CompressBlockBC6((uint16_t *)inblock.data(), 4 * 3, block, bc6opts);
        else if(fmt.type == ResourceFormatType::BC7)
          CompressBlockBC7(inblock.data(), 4 * sizeof(uint32_t), block, NULL);

        ret.append(block, blockSize);
      }

      ret.resize(ret.size() + (rowPitch - tightPitch));
    }

    DestroyOptionsBC6(bc6opts);
#endif
  }
  else if(fmt.type == ResourceFormatType::ETC2 || fmt.type == ResourceFormatType::EAC ||
          fmt.type == ResourceFormatType::ASTC || fmt.type == ResourceFormatType::PVRTC ||
          fmt.type == ResourceFormatType::YUV8 || fmt.type == ResourceFormatType::YUV10 ||
          fmt.type == ResourceFormatType::YUV12 || fmt.type == ResourceFormatType::YUV16)
  {
    RDCERR("Format %s not supported for proper discard pattern", fmt.Name().c_str());
  }
  else
  {
    RDCERR("Unhandled format %s needing discard pattern", fmt.Name().c_str());
  }

  // if we didn't get a proper pattern, try at least to do some kind of checkerboard (not knowing if
  // this will align with the format or not)
  if(ret.empty())
  {
    uint32_t tightPitch = DiscardPatternWidth * 16;
    rowPitch = RDCMAX(rowPitch, tightPitch);

    ret.resize(rowPitch * DiscardPatternHeight);

    byte *out = ret.data();
    int val = 0;
    for(uint32_t y = 0; y < DiscardPatternHeight; y++)
    {
      byte *rowout = out;

      for(uint32_t i = 0; i < DiscardPatternWidth; i++)
      {
        memset(rowout, val, 16);
        rowout += 16;

        // toggle between memset(0) and memset(0xff)
        if(val)
          val = 0;
        else
          val = 0xff;
      }

      out += rowPitch;
    }
  }

  return ret;
}

void DeriveNearFar(Vec4f pos, Vec4f pos0, float &nearp, float &farp, bool &found)
{
  //////////////////////////////////////////////////////////////////////////////////
  // derive near/far, assuming a standard perspective matrix
  //
  // the transformation from from pre-projection {Z,W} to post-projection {Z,W}
  // is linear. So we can say Zpost = Zpre*m + c . Here we assume Wpre = 1
  // and we know Wpost = Zpre from the perspective matrix.
  // we can then see from the perspective matrix that
  // m = F/(F-N)
  // c = -(F*N)/(F-N)
  //
  // with re-arranging and substitution, we then get:
  // N = -c/m
  // F = c/(1-m)
  //
  // so if we can derive m and c then we can determine N and F. We can do this with
  // two points, and we pick them reasonably distinct on z to reduce floating-point
  // error

  // skip invalid vertices (w=0)
  if(pos.w != 0.0f && fabsf(pos.w - pos0.w) > 0.01f && fabsf(pos.z - pos0.z) > 0.01f)
  {
    Vec2f A(pos0.w, pos0.z);
    Vec2f B(pos.w, pos.z);

    float m = (B.y - A.y) / (B.x - A.x);
    float c = B.y - B.x * m;

    if(m == 1.0f || c == 0.0f)
      return;

    if(-c / m <= 0.000001f)
      return;

    nearp = -c / m;
    farp = c / (1 - m);

    found = true;
  }
}

#if ENABLED(ENABLE_UNIT_TESTS)

#include "catch/catch.hpp"

TEST_CASE("Check PreprocessLineDirectives", "[preprocess]")
{
  rdcarray<ShaderSourceFile> sources;

  SECTION("Identity case, one file")
  {
    sources.push_back({"file.hlsl", "float4 foo;"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == "float4 foo;");
  }

  SECTION("Identity case, two files")
  {
    sources.push_back({"file.hlsl", "float4 foo;"});
    sources.push_back({"file2.hlsl", "int4 bar;"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == "float4 foo;");
    CHECK(sources[1].filename == "file2.hlsl");
    CHECK(sources[1].contents == "int4 bar;");
  }

  SECTION("Identity case, empty filename")
  {
    sources.push_back({"file.hlsl", "float4 foo;"});
    sources.push_back({"file2.hlsl", "int4 bar;"});
    sources.push_back({"", "struct blah {};"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == "float4 foo;");
    CHECK(sources[1].filename == "file2.hlsl");
    CHECK(sources[1].contents == "int4 bar;");
    CHECK(sources[2].filename == "");
    CHECK(sources[2].contents == "struct blah {};");
  }

  SECTION("Line number only, identity expansion")
  {
    sources.push_back({"file.hlsl", R"(#line 2
float4 foo;
#line 4
int4 bar;)"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == R"(
float4 foo;

int4 bar;)");
  }

  SECTION("Line number only, forward expansion")
  {
    sources.push_back({"file.hlsl", R"(#line 2
float4 foo;
#line 10
int4 bar;)"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == R"(
float4 foo;







int4 bar;)");
  }

  SECTION("Line number reversal")
  {
    sources.push_back({"file.hlsl", R"(#line 2
float4 foo;





#line 3
int4 bar;)"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == R"(
float4 foo;
int4 bar;)");
  }

  SECTION("Line number overlap")
  {
    sources.push_back({"file.hlsl", R"(#line 2
float4 foo;





#line 2
int4 bar;)"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == R"(
float4 foo; /* multiple #lines overlapping */ int4 bar;)");
  }

  SECTION("Line and filename, single file")
  {
    rdcstr merged = R"(#line 2 "file.hlsl"
float4 foo;


#line 10 "file.hlsl"
int4 bar;)";

    sources.push_back({"merged.hlsl", merged});
    sources.push_back({"file.hlsl", ""});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged.hlsl");
    CHECK(sources[0].contents == merged);
    CHECK(sources[1].filename == "file.hlsl");
    CHECK(sources[1].contents == R"(
float4 foo;







int4 bar;)");
  }

  SECTION("Line and filename, unreferenced file")
  {
    rdcstr merged = R"(#line 2 "file.hlsl"
float4 foo;


#line 10 "unknown.hlsl"
int4 bar;)";

    sources.push_back({"merged.hlsl", merged});
    sources.push_back({"file.hlsl", ""});

    // unknown.hlsl needs to be in the sources list to be filled in, shader debug information is
    // assumed to at least include an empty referenced filename for used files

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged.hlsl");
    CHECK(sources[0].contents == merged);
    CHECK(sources[1].filename == "file.hlsl");
    CHECK(sources[1].contents == R"(
float4 foo;)");
  }

  SECTION("Line and filename, multiple files")
  {
    rdcstr merged = R"(#line 2 "file.hlsl"
float4 foo;


#line 10 "file2.hlsl"
int4 bar;


#line 10 "file.hlsl"
struct blah {};)";

    sources.push_back({"merged.hlsl", merged});
    sources.push_back({"file.hlsl", ""});
    sources.push_back({"file2.hlsl", ""});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged.hlsl");
    CHECK(sources[0].contents == merged);
    CHECK(sources[1].filename == "file.hlsl");
    CHECK(sources[1].contents == R"(
float4 foo;







struct blah {};)");
    CHECK(sources[2].filename == "file2.hlsl");
    CHECK(sources[2].contents == R"(








int4 bar;)");
  }

  SECTION("Many-to-many file expansions")
  {
    rdcstr merged1 = R"(#line 2 "file.hlsl"
float4 foo;


#line 10 "file2.hlsl"
int4 bar;


#line 10 "file.hlsl"
struct blah {};)";

    rdcstr merged2 = R"(#line 3 "file.hlsl"
// this was foo


#line 11 "file2.hlsl"
// this was bar


#line 11 "file.hlsl"
// this was a struct)";

    sources.push_back({"merged1.hlsl", merged1});
    sources.push_back({"merged2.hlsl", merged2});
    sources.push_back({"file.hlsl", ""});
    sources.push_back({"file2.hlsl", ""});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged1.hlsl");
    CHECK(sources[0].contents == merged1);
    CHECK(sources[1].filename == "merged2.hlsl");
    CHECK(sources[1].contents == merged2);
    CHECK(sources[2].filename == "file.hlsl");
    CHECK(sources[2].contents == R"(
float4 foo;
// this was foo






struct blah {};
// this was a struct)");
    CHECK(sources[3].filename == "file2.hlsl");
    CHECK(sources[3].contents == R"(








int4 bar;
// this was bar)");
  }

  SECTION("Preamble, with only line number changes")
  {
    sources.push_back({"file.hlsl", R"(
// preamble
#line 5
float4 foo;





#line 6
int4 bar;)"});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "file.hlsl");
    CHECK(sources[0].contents == R"(
// preamble


float4 foo;
int4 bar;)");
  }

  SECTION("Preamble with different filenames")
  {
    rdcstr merged = R"(
// merged preamble

#line 2 "file.hlsl"
float4 foo;


#line 10 "file2.hlsl"
int4 bar;


#line 10 "file.hlsl"
struct blah {};)";

    sources.push_back({"merged.hlsl", merged});
    sources.push_back({"file.hlsl", ""});
    sources.push_back({"file2.hlsl", ""});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged.hlsl");
    CHECK(sources[0].contents == merged);
    CHECK(sources[1].filename == "file.hlsl");
    CHECK(sources[1].contents == R"(
float4 foo;







struct blah {};)");
    CHECK(sources[2].filename == "file2.hlsl");
    CHECK(sources[2].contents == R"(








int4 bar;)");
  }

  SECTION("Preamble with self filename")
  {
    rdcstr merged = R"(
// merged preamble

#line 5 "merged.hlsl"
float4 foo;


#line 10 "merged.hlsl"
int4 bar;)";

    sources.push_back({"merged.hlsl", merged});

    PreprocessLineDirectives(sources);

    CHECK(sources[0].filename == "merged.hlsl");
    CHECK(sources[0].contents == R"(
// merged preamble


float4 foo;




int4 bar;)");
  }
}

#endif
