<commit_before>/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 <NAME>
 *
 * 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 "BufferViewer.h"
#include <float.h>
#include <QDoubleSpinBox>
#include <QFontDatabase>
#include <QMenu>
#include <QMouseEvent>
#include <QScrollBar>
#include <QTimer>
#include <QtMath>
#include "Code/Resources.h"
#include "Windows/ShaderViewer.h"
#include "ui_BufferViewer.h"

class CameraWrapper
{
public:
  virtual ~CameraWrapper() {}
  virtual bool Update(QRect winSize) = 0;
  virtual Camera *camera() = 0;

  virtual void MouseWheel(QWheelEvent *e) = 0;

  virtual void MouseClick(QMouseEvent *e) { m_DragStartPos = e->pos(); }
  virtual void MouseMove(QMouseEvent *e)
  {
    if(e->buttons() & Qt::LeftButton)
    {
      if(m_DragStartPos.x() < 0)
      {
        m_DragStartPos = e->pos();
      }

      m_DragStartPos = e->pos();
    }
    else
    {
      m_DragStartPos = QPoint(-1, -1);
    }
  }

  virtual void KeyUp(QKeyEvent *e)
  {
    if(e->key() == Qt::Key_A || e->key() == Qt::Key_D)
      setMove(Direction::Horiz, 0);
    if(e->key() == Qt::Key_Q || e->key() == Qt::Key_E)
      setMove(Direction::Vert, 0);
    if(e->key() == Qt::Key_W || e->key() == Qt::Key_S)
      setMove(Direction::Fwd, 0);

    if(e->modifiers() && Qt::ShiftModifier)
      m_CurrentSpeed = 3.0f;
    else
      m_CurrentSpeed = 1.0f;
  }

  virtual void KeyDown(QKeyEvent *e)
  {
    if(e->key() == Qt::Key_W)
      setMove(Direction::Fwd, 1);
    if(e->key() == Qt::Key_S)
      setMove(Direction::Fwd, -1);
    if(e->key() == Qt::Key_Q)
      setMove(Direction::Vert, 1);
    if(e->key() == Qt::Key_E)
      setMove(Direction::Vert, -1);
    if(e->key() == Qt::Key_D)
      setMove(Direction::Horiz, 1);
    if(e->key() == Qt::Key_A)
      setMove(Direction::Horiz, -1);

    if(e->modifiers() && Qt::ShiftModifier)
      m_CurrentSpeed = 3.0f;
    else
      m_CurrentSpeed = 1.0f;
  }

  float SpeedMultiplier = 0.05f;

protected:
  enum class Direction
  {
    Fwd,
    Horiz,
    Vert,
    Num
  };

  int move(Direction dir) { return m_CurrentMove[(int)dir]; }
  float currentSpeed() { return m_CurrentSpeed * SpeedMultiplier; }
  QPoint dragStartPos() { return m_DragStartPos; }
private:
  float m_CurrentSpeed = 1.0f;
  int m_CurrentMove[(int)Direction::Num] = {0, 0, 0};

  void setMove(Direction dir, int val) { m_CurrentMove[(int)dir] = val; }
  QPoint m_DragStartPos = QPoint(-1, -1);
};

class ArcballWrapper : public CameraWrapper
{
public:
  ArcballWrapper() { m_Cam = Camera_InitArcball(); }
  virtual ~ArcballWrapper() { Camera_Shutdown(m_Cam); }
  Camera *camera() override { return m_Cam; }
  void Reset(FloatVector pos, float dist)
  {
    Camera_ResetArcball(m_Cam);

    setLookAtPos(pos);
    SetDistance(dist);
  }

  void SetDistance(float dist)
  {
    m_Distance = qAbs(dist);
    Camera_SetArcballDistance(m_Cam, m_Distance);
  }

  bool Update(QRect size) override
  {
    m_WinSize = size;
    return false;
  }

  void MouseWheel(QWheelEvent *e) override
  {
    float mod = (1.0f - e->delta() / 2500.0f);

    SetDistance(qMax(1e-6f, m_Distance * mod));
  }

  void MouseMove(QMouseEvent *e) override
  {
    if(dragStartPos().x() > 0)
    {
      if(e->buttons() == Qt::MiddleButton ||
         (e->buttons() == Qt::LeftButton && e->modifiers() & Qt::AltModifier))
      {
        float xdelta = (float)(e->pos().x() - dragStartPos().x()) / 300.0f;
        float ydelta = (float)(e->pos().y() - dragStartPos().y()) / 300.0f;

        xdelta *= qMax(1.0f, m_Distance);
        ydelta *= qMax(1.0f, m_Distance);

        FloatVector pos, fwd, right, up;
        Camera_GetBasis(m_Cam, &pos, &fwd, &right, &up);

        m_LookAt.x -= right.x * xdelta;
        m_LookAt.y -= right.y * xdelta;
        m_LookAt.z -= right.z * xdelta;

        m_LookAt.x += up.x * ydelta;
        m_LookAt.y += up.y * ydelta;
        m_LookAt.z += up.z * ydelta;

        Camera_SetPosition(m_Cam, m_LookAt.x, m_LookAt.y, m_LookAt.z);
      }
      else if(e->buttons() == Qt::LeftButton)
      {
        RotateArcball(dragStartPos(), e->pos());
      }
    }

    CameraWrapper::MouseMove(e);
  }

  FloatVector lookAtPos() { return m_LookAt; }
  void setLookAtPos(const FloatVector &v)
  {
    m_LookAt = v;
    Camera_SetPosition(m_Cam, v.x, v.y, v.z);
  }

private:
  Camera *m_Cam;

  QRect m_WinSize;

  float m_Distance = 10.0f;
  FloatVector m_LookAt;

  void RotateArcball(QPoint from, QPoint to)
  {
    float ax = ((float)from.x() / (float)m_WinSize.width()) * 2.0f - 1.0f;
    float ay = ((float)from.y() / (float)m_WinSize.height()) * 2.0f - 1.0f;
    float bx = ((float)to.x() / (float)m_WinSize.width()) * 2.0f - 1.0f;
    float by = ((float)to.y() / (float)m_WinSize.height()) * 2.0f - 1.0f;

    // this isn't a 'true arcball' but it handles extreme aspect ratios
    // better. We basically 'centre' around the from point always being
    // 0,0 (straight out of the screen) as if you're always dragging
    // the arcball from the middle, and just use the relative movement
    int minDimension = qMin(m_WinSize.width(), m_WinSize.height());

    ax = ay = 0;
    bx = ((float)(to.x() - from.x()) / (float)minDimension) * 2.0f;
    by = ((float)(to.y() - from.y()) / (float)minDimension) * 2.0f;

    ay = -ay;
    by = -by;

    Camera_RotateArcball(m_Cam, ax, ay, bx, by);
  }
};

class FlycamWrapper : public CameraWrapper
{
public:
  FlycamWrapper() { m_Cam = Camera_InitFPSLook(); }
  virtual ~FlycamWrapper() { Camera_Shutdown(m_Cam); }
  Camera *camera() override { return m_Cam; }
  void Reset(FloatVector pos)
  {
    m_Position = pos;
    m_Rotation = FloatVector();

    Camera_SetPosition(m_Cam, m_Position.x, m_Position.y, m_Position.z);
    Camera_SetFPSRotation(m_Cam, m_Rotation.x, m_Rotation.y, m_Rotation.z);
  }

  bool Update(QRect size) override
  {
    FloatVector pos, fwd, right, up;
    Camera_GetBasis(m_Cam, &pos, &fwd, &right, &up);

    float speed = currentSpeed();

    int horizMove = move(CameraWrapper::Direction::Horiz);
    if(horizMove)
    {
      m_Position.x += right.x * speed * (float)horizMove;
      m_Position.y += right.y * speed * (float)horizMove;
      m_Position.z += right.z * speed * (float)horizMove;
    }

    int vertMove = move(CameraWrapper::Direction::Vert);
    if(vertMove)
    {
      // this makes less intuitive sense, instead go 'absolute' up
      // m_Position.x += up.x * speed * (float)vertMove;
      // m_Position.y += up.y * speed * (float)vertMove;
      // m_Position.z += up.z * speed * (float)vertMove;

      m_Position.y += speed * (float)vertMove;
    }

    int fwdMove = move(CameraWrapper::Direction::Fwd);
    if(fwdMove)
    {
      m_Position.x += fwd.x * speed * (float)fwdMove;
      m_Position.y += fwd.y * speed * (float)fwdMove;
      m_Position.z += fwd.z * speed * (float)fwdMove;
    }

    if(horizMove || vertMove || fwdMove)
    {
      Camera_SetPosition(m_Cam, m_Position.x, m_Position.y, m_Position.z);
      return true;
    }

    return false;
  }

  void MouseWheel(QWheelEvent *e) override {}
  void MouseMove(QMouseEvent *e) override
  {
    if(dragStartPos().x() > 0 && e->buttons() == Qt::LeftButton)
    {
      m_Rotation.y -= (float)(e->pos().x() - dragStartPos().x()) / 300.0f;
      m_Rotation.x -= (float)(e->pos().y() - dragStartPos().y()) / 300.0f;

      Camera_SetFPSRotation(m_Cam, m_Rotation.x, m_Rotation.y, m_Rotation.z);
    }

    CameraWrapper::MouseMove(e);
  }

private:
  Camera *m_Cam;

  FloatVector m_Position, m_Rotation;
};

struct BufferData
{
  BufferData()
  {
    refcount.store(1);
    data = end = NULL;
    stride = 0;
  }

  void ref() { refcount.ref(); }
  void deref()
  {
    bool alive = refcount.deref();

    if(!alive)
    {
      delete[] data;
      delete this;
    }
  }

  QAtomicInteger<uint32_t> refcount;
  byte *data;
  byte *end;
  size_t stride;
};

uint32_t CalcIndex(BufferData *data, uint32_t vertID, int32_t baseVertex)
{
  byte *idxData = data->data + vertID * sizeof(uint32_t);
  if(idxData + sizeof(uint32_t) > data->end)
    return ~0U;

  uint32_t idx = *(uint32_t *)idxData;

  // apply base vertex but clamp to 0 if subtracting
  if(baseVertex < 0)
  {
    uint32_t subtract = (uint32_t)(-baseVertex);

    if(idx < subtract)
      idx = 0;
    else
      idx -= subtract;
  }
  else if(baseVertex > 0)
  {
    idx += (uint32_t)baseVertex;
  }

  return idx;
}

class BufferItemModel : public QAbstractItemModel
{
public:
  BufferItemModel(RDTableView *v, QObject *parent) : QAbstractItemModel(parent)
  {
    view = v;
    view->setModel(this);
  }
  void beginReset() { emit beginResetModel(); }
  void endReset()
  {
    cacheColumns();
    m_ColumnCount = columnLookup.count() + reservedColumnCount();
    emit endResetModel();
  }
  QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
  {
    if(row < 0 || row >= rowCount())
      return QModelIndex();

    return createIndex(row, column);
  }

  QModelIndex parent(const QModelIndex &index) const override { return QModelIndex(); }
  int rowCount(const QModelIndex &parent = QModelIndex()) const override { return numRows; }
  int columnCount(const QModelIndex &parent = QModelIndex()) const override
  {
    return m_ColumnCount;
  }
  Qt::ItemFlags flags(const QModelIndex &index) const override
  {
    if(!index.isValid())
      return 0;

    return QAbstractItemModel::flags(index);
  }

  QVariant headerData(int section, Qt::Orientation orientation, int role) const override
  {
    if(section < m_ColumnCount && orientation == Qt::Horizontal && role == Qt::DisplayRole)
    {
      if(section == 0 && meshView)
      {
        return "VTX";
      }
      else if(section == 1 && meshView)
      {
        return "IDX";
      }
      else
      {
        const FormatElement &el = elementForColumn(section);

        if(el.format.compCount == 1)
          return el.name;

        QChar comps[] = {'x', 'y', 'z', 'w'};

        return QString("%1.%2").arg(el.name).arg(comps[componentForIndex(section)]);
      }
    }

    return QVariant();
  }

  QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
  {
    if(index.isValid())
    {
      if(role == Qt::SizeHintRole)
      {
        QStyleOptionViewItem opt = view->viewOptions();
        opt.features |= QStyleOptionViewItem::HasDisplay;

        // pad these columns to allow for sufficiently wide data
        if(index.column() < 2 && meshView)
          opt.text = "999999";
        else
          opt.text = data(index).toString();
        opt.styleObject = NULL;

        QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
        return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), opt.widget);
      }

      uint32_t row = index.row();
      int col = index.column();

      if((role == Qt::BackgroundRole || role == Qt::ForegroundRole) && col >= reservedColumnCount())
      {
        int elIdx = columnLookup[col - reservedColumnCount()];
        int compIdx = componentForIndex(col);
        if(elIdx == positionEl)
        {
          if(role == Qt::ForegroundRole)
            return QBrush(Qt::black);

          if(compIdx != 3 || !meshInput)
          {
            // C# SkyBlue
            return QBrush(QColor::fromRgb(135, 206, 235));
          }
          else
          {
            // C# LightCyan
            return QBrush(QColor::fromRgb(224, 255, 255));
          }
        }
        else if(secondaryEnabled && elIdx == secondaryEl)
        {
          if(role == Qt::ForegroundRole)
            return QBrush(Qt::black);

          if((secondaryElAlpha && compIdx == 3) || (!secondaryElAlpha && compIdx != 3))
          {
            // C# LightGreen
            return QBrush(QColor::fromRgb(144, 238, 144));
          }
          else
          {
            return QBrush(QColor::fromRgb(200, 238, 200));
          }
        }
      }

      if(role == Qt::DisplayRole)
      {
        if(col >= 0 && col < m_ColumnCount && row < numRows)
        {
          if(col == 0 && meshView)
            return row;

          uint32_t idx = row;

          if(indices && indices->data)
          {
            idx = CalcIndex(indices, row, baseVertex);

            if(idx == ~0U)
              return QVariant();
          }

          if(col == 1 && meshView)
            return idx;

          const FormatElement &el = elementForColumn(col);

          uint32_t instIdx = 0;
          if(el.instancerate > 0)
            instIdx = curInstance / el.instancerate;

          if(el.buffer < buffers.size())
          {
            const byte *data = buffers[el.buffer]->data;
            const byte *end = buffers[el.buffer]->end;

            if(!el.perinstance)
              data += buffers[el.buffer]->stride * idx;
            else
              data += buffers[el.buffer]->stride * instIdx;

            data += el.offset;

            // only slightly wasteful, we need to fetch all variants together
            // since some formats are packed and can't be read individually
            QVariantList list = el.GetVariants(data, end);

            int comp = componentForIndex(col);

            if(comp < list.count())
            {
              QVariant &v = list[comp];

              QString ret;

              QMetaType::Type vt = (QMetaType::Type)v.type();

              if(vt == QMetaType::Double)
              {
                double d = v.toDouble();
                // pad with space on left if sign is missing, to better align
                if(d < 0.0)
                  ret = Formatter::Format(d);
                else if(d > 0.0)
                  ret = " " + Formatter::Format(d);
                else if(qIsNaN(d))
                  ret = " NaN";
                else
                  // force negative and positive 0 together
                  ret = " " + Formatter::Format(0.0);
              }
              else if(vt == QMetaType::Float)
              {
                float f = v.toFloat();
                // pad with space on left if sign is missing, to better align
                if(f < 0.0)
                  ret = Formatter::Format(f);
                else if(f > 0.0)
                  ret = " " + Formatter::Format(f);
                else if(qIsNaN(f))
                  ret = " NaN";
                else
                  // force negative and positive 0 together
                  ret = " " + Formatter::Format(0.0);
              }
              else if(vt == QMetaType::UInt || vt == QMetaType::UShort || vt == QMetaType::UChar)
              {
                ret = Formatter::Format(v.toUInt(), el.hex);
              }
              else if(vt == QMetaType::Int || vt == QMetaType::Short || vt == QMetaType::SChar)
              {
                int i = v.toInt();
                if(i > 0)
                  ret = " " + Formatter::Format(i);
                else
                  ret = Formatter::Format(i);
              }
              else
                ret = v.toString();

              return ret;
            }
          }
        }
      }
    }

    return QVariant();
  }

  RDTableView *view = NULL;

  int32_t baseVertex = 0;
  uint32_t curInstance = 0;
  uint32_t numRows = 0;
  bool meshView = true;
  bool meshInput = false;
  BufferData *indices = NULL;
  QList<FormatElement> columns;
  QList<BufferData *> buffers;

  void setPosColumn(int pos)
  {
    QVector<int> roles = {Qt::BackgroundRole, Qt::ForegroundRole};

    if(positionEl != pos)
    {
      if(positionEl >= 0)
        emit dataChanged(index(0, firstColumnForElement(positionEl)),
                         index(rowCount() - 1, lastColumnForElement(positionEl)), roles);

      if(pos >= 0)
        emit dataChanged(index(0, firstColumnForElement(pos)),
                         index(rowCount() - 1, lastColumnForElement(pos)), roles);
    }

    positionEl = pos;
  }

  int posColumn() { return positionEl; }
  QString posName()
  {
    if(positionEl >= 0 && positionEl < columns.count())
      return columns[positionEl].name;

    return "";
  }

  void setSecondaryColumn(int sec, bool secEnabled, bool secAlpha)
  {
    QVector<int> roles = {Qt::BackgroundRole, Qt::ForegroundRole};

    if(secondaryEl != sec || secondaryElAlpha != secAlpha || secondaryEnabled != secEnabled)
    {
      if(secondaryEl >= 0 && secondaryEl != sec)
        emit dataChanged(index(0, firstColumnForElement(secondaryEl)),
                         index(rowCount() - 1, lastColumnForElement(secondaryEl)), roles);

      if(sec >= 0)
        emit dataChanged(index(0, firstColumnForElement(sec)),
                         index(rowCount() - 1, lastColumnForElement(sec)), roles);
    }

    secondaryEl = sec;
    secondaryElAlpha = secAlpha;
    secondaryEnabled = secEnabled;
  }

  int secondaryColumn() { return secondaryEl; }
  bool secondaryAlpha() { return secondaryElAlpha; }
  QString secondaryName()
  {
    if(secondaryEl >= 0 && secondaryEl < columns.count())
      return columns[secondaryEl].name;

    return "";
  }
  int elementIndexForColumn(int col) const
  {
    if(col < reservedColumnCount())
      return -1;

    return columnLookup[col - reservedColumnCount()];
  }

  const FormatElement &elementForColumn(int col) const
  {
    return columns[columnLookup[col - reservedColumnCount()]];
  }

private:
  // maps from column number (0-based from data, so excluding VTX/IDX columns)
  // to the column element in the columns list, and lists its component.
  //
  // So a float4, float3, int set of columns would be:
  // { 0, 0, 0, 0, 1, 1, 1, 2 };
  // { 0, 1, 2, 3, 0, 1, 2, 0 };
  QVector<int> columnLookup;
  QVector<int> componentLookup;
  int m_ColumnCount = 0;

  int positionEl = -1;
  int secondaryEl = -1;
  bool secondaryElAlpha = false;
  bool secondaryEnabled = false;

  int reservedColumnCount() const { return (meshView ? 2 : 0); }
  int componentForIndex(int col) const { return componentLookup[col - reservedColumnCount()]; }
  int firstColumnForElement(int el) const
  {
    for(int i = 0; i < columnLookup.count(); i++)
    {
      if(columnLookup[i] == el)
        return reservedColumnCount() + i;
    }

    return 0;
  }

  int lastColumnForElement(int el) const
  {
    for(int i = columnLookup.count() - 1; i >= 0; i--)
    {
      if(columnLookup[i] == el)
        return reservedColumnCount() + i;
    }

    return columnCount() - 1;
  }

  void cacheColumns()
  {
    columnLookup.clear();
    columnLookup.reserve(columns.count() * 4);
    componentLookup.clear();
    componentLookup.reserve(columns.count() * 4);

    for(int i = 0; i < columns.count(); i++)
    {
      FormatElement &fmt = columns[i];

      uint32_t compCount;

      switch(fmt.format.specialFormat)
      {
        case eSpecial_BC6:
        case eSpecial_ETC2:
        case eSpecial_R11G11B10:
        case eSpecial_R5G6B5:
        case eSpecial_R9G9B9E5: compCount = 3; break;
        case eSpecial_BC1:
        case eSpecial_BC7:
        case eSpecial_BC3:
        case eSpecial_BC2:
        case eSpecial_R10G10B10A2:
        case eSpecial_R5G5B5A1:
        case eSpecial_R4G4B4A4:
        case eSpecial_ASTC: compCount = 4; break;
        case eSpecial_BC5:
        case eSpecial_R4G4:
        case eSpecial_D16S8:
        case eSpecial_D24S8:
        case eSpecial_D32S8: compCount = 2; break;
        case eSpecial_BC4:
        case eSpecial_S8: compCount = 1; break;
        case eSpecial_YUV:
        case eSpecial_EAC:
        default: compCount = fmt.format.compCount;
      }

      for(uint32_t c = 0; c < compCount; c++)
      {
        columnLookup.push_back(i);
        componentLookup.push_back((int)c);
      }
    }
  }
};

struct CachedElData
{
  const FormatElement *el = NULL;

  const byte *data = NULL;
  const byte *end = NULL;

  size_t stride;
  int byteSize;
  uint32_t instIdx = 0;

  QByteArray nulls;
};

void CacheDataForIteration(QVector<CachedElData> &cache, const QList<FormatElement> &columns,
                           const QList<BufferData *> buffers, uint32_t inst)
{
  cache.reserve(columns.count());

  for(int col = 0; col < columns.count(); col++)
  {
    const FormatElement &el = columns[col];

    CachedElData d;

    d.el = &el;

    d.byteSize = el.byteSize();
    d.nulls = QByteArray(d.byteSize, '\0');

    if(el.instancerate > 0)
      d.instIdx = inst / el.instancerate;

    if(el.buffer < buffers.size())
    {
      d.data = buffers[el.buffer]->data;
      d.end = buffers[el.buffer]->end;

      d.stride = buffers[el.buffer]->stride;

      d.data += el.offset;

      if(el.perinstance)
        d.data += d.stride * d.instIdx;
    }

    cache.push_back(d);
  }
}

BufferViewer::BufferViewer(CaptureContext &ctx, bool meshview, QWidget *parent)
    : QFrame(parent), ui(new Ui::BufferViewer), m_Ctx(ctx)
{
  ui->setupUi(this);

  m_ModelVSIn = new BufferItemModel(ui->vsinData, this);
  m_ModelVSOut = new BufferItemModel(ui->vsoutData, this);
  m_ModelGSOut = new BufferItemModel(ui->gsoutData, this);

  m_Flycam = new FlycamWrapper();
  m_Arcball = new ArcballWrapper();
  m_CurrentCamera = m_Arcball;

  m_Output = NULL;

  memset(&m_Config, 0, sizeof(m_Config));
  m_Config.type = eMeshDataStage_VSIn;
  m_Config.wireframeDraw = true;

  ui->outputTabs->setCurrentIndex(0);
  m_CurStage = eMeshDataStage_VSIn;

  ui->vsinData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
  ui->vsoutData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
  ui->gsoutData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));

  m_ModelVSIn->meshView = m_ModelVSOut->meshView = m_ModelGSOut->meshView = m_MeshView = meshview;

  m_ModelVSIn->meshInput = true;

  if(meshview)
    SetupMeshView();
  else
    SetupRawView();

  QMenu *exportMenu = new QMenu(this);

  QAction *csv = new QAction(tr("Export to &CSV"), this);
  csv->setIcon(Icons::save());
  exportMenu->addAction(csv);
  QAction *bytes = new QAction(tr("Export to &Bytes"), this);
  bytes->setIcon(Icons::save());
  exportMenu->addAction(bytes);

  QAction *debug = new QAction(tr("&Debug this Vertex"), this);
  debug->setIcon(Icons::wrench());

  ui->exportDrop->setMenu(exportMenu);

  QObject::connect(csv, &QAction::triggered, [this] { exportData(BufferExport(BufferExport::CSV)); });
  QObject::connect(bytes, &QAction::triggered,
                   [this] { exportData(BufferExport(BufferExport::RawBytes)); });
  QObject::connect(debug, &QAction::triggered, this, &BufferViewer::debugVertex);

  QObject::connect(ui->exportDrop, &QToolButton::clicked,
                   [this] { exportData(BufferExport(BufferExport::CSV)); });

  ui->vsinData->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->vsoutData->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->gsoutData->setContextMenuPolicy(Qt::CustomContextMenu);

  QMenu *vsinMenu = new QMenu(this);

  QObject::connect(ui->vsinData, &RDTableView::customContextMenuRequested,
                   [this, vsinMenu, debug, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->vsinData;

                     vsinMenu->clear();

                     if(m_MeshView)
                     {
                       vsinMenu->addAction(debug);
                       vsinMenu->addSeparator();
                     }

                     vsinMenu->addAction(csv);
                     vsinMenu->addAction(bytes);

                     vsinMenu->popup(ui->vsinData->viewport()->mapToGlobal(pos));
                   });

  QMenu *vsoutMenu = new QMenu(this);

  QObject::connect(ui->vsoutData, &RDTableView::customContextMenuRequested,
                   [this, vsoutMenu, debug, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->vsoutData;

                     vsoutMenu->clear();

                     if(m_MeshView)
                     {
                       vsoutMenu->addAction(debug);
                       vsoutMenu->addSeparator();
                     }

                     vsoutMenu->addAction(csv);
                     vsoutMenu->addAction(bytes);

                     vsoutMenu->popup(ui->vsoutData->viewport()->mapToGlobal(pos));
                   });

  QMenu *gsoutMenu = new QMenu(this);

  QObject::connect(ui->gsoutData, &RDTableView::customContextMenuRequested,
                   [this, gsoutMenu, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->gsoutData;

                     gsoutMenu->clear();

                     gsoutMenu->addAction(csv);
                     gsoutMenu->addAction(bytes);

                     gsoutMenu->popup(ui->gsoutData->viewport()->mapToGlobal(pos));
                   });

  ui->dockarea->setAllowFloatingWindow(false);
  ui->dockarea->setRubberBandLineWidth(50);

  ui->controlType->addItems({tr("Arcball"), tr("WASD")});
  ui->controlType->adjustSize();

  ui->drawRange->addItems({tr("Only this draw"), tr("Show previous instances"),
                           tr("Show all instances"), tr("Show whole pass")});
  ui->drawRange->adjustSize();
  ui->drawRange->setCurrentIndex(0);

  ui->solidShading->addItems({tr("None"), tr("Solid Colour"), tr("Flat Shaded"), tr("Secondary")});
  ui->solidShading->adjustSize();
  ui->solidShading->setCurrentIndex(0);

  // wireframe only available on solid shaded options
  ui->wireframeRender->setEnabled(false);

  ui->fovGuess->setValue(90.0);

  on_controlType_currentIndexChanged(0);

  QObject::connect(ui->vsinData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);
  QObject::connect(ui->vsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);
  QObject::connect(ui->gsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);

  QObject::connect(ui->vsinData, &RDTableView::clicked, [this]() { m_CurView = ui->vsinData; });
  QObject::connect(ui->vsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->vsoutData; });
  QObject::connect(ui->gsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->gsoutData; });

  QObject::connect(ui->vsinData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);
  QObject::connect(ui->vsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);
  QObject::connect(ui->gsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);

  QObject::connect(ui->fovGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->aspectGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->nearGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->farGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->matrixType, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged),
                   [this](int) { camGuess_changed(0.0); });

  Reset();

  m_Ctx.AddLogViewer(this);
}

void BufferViewer::SetupRawView()
{
  ui->formatSpecifier->setVisible(true);
  ui->outputTabs->setVisible(false);
  ui->vsoutData->setVisible(false);
  ui->gsoutData->setVisible(false);

  // hide buttons we don't want in the toolbar
  ui->syncViews->setVisible(false);
  ui->offsetLine->setVisible(false);
  ui->instanceLabel->setVisible(false);
  ui->instance->setVisible(false);
  ui->rowOffsetLabel->setVisible(false);
  ui->rowOffset->setVisible(false);

  ui->vsinData->setWindowTitle(tr("Buffer Contents"));
  ui->dockarea->addToolWindow(ui->vsinData, ToolWindowManager::EmptySpace);
  ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);

  ui->formatSpecifier->setWindowTitle(tr("Buffer Format"));
  ui->dockarea->addToolWindow(ui->formatSpecifier, ToolWindowManager::AreaReference(
                                                       ToolWindowManager::BottomOf,
                                                       ui->dockarea->areaOf(ui->vsinData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->formatSpecifier, ToolWindowManager::HideCloseButton);

  QObject::connect(ui->formatSpecifier, &BufferFormatSpecifier::processFormat, this,
                   &BufferViewer::processFormat);

  QVBoxLayout *vertical = new QVBoxLayout(this);

  vertical->setSpacing(3);
  vertical->setContentsMargins(0, 0, 0, 0);

  vertical->addWidget(ui->meshToolbar);
  vertical->addWidget(ui->dockarea);
}

void BufferViewer::SetupMeshView()
{
  setWindowTitle(tr("Mesh Output"));

  ui->formatSpecifier->setVisible(false);
  ui->cameraControlsGroup->setVisible(false);

  ui->outputTabs->setWindowTitle(tr("Preview"));
  ui->dockarea->addToolWindow(ui->outputTabs, ToolWindowManager::EmptySpace);
  ui->dockarea->setToolWindowProperties(ui->outputTabs, ToolWindowManager::HideCloseButton);

  ui->vsinData->setWindowTitle(tr("VS Input"));
  ui->dockarea->addToolWindow(
      ui->vsinData, ToolWindowManager::AreaReference(ToolWindowManager::TopOf,
                                                     ui->dockarea->areaOf(ui->outputTabs), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);

  ui->vsoutData->setWindowTitle(tr("VS Output"));
  ui->dockarea->addToolWindow(
      ui->vsoutData, ToolWindowManager::AreaReference(ToolWindowManager::RightOf,
                                                      ui->dockarea->areaOf(ui->vsinData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->vsoutData, ToolWindowManager::HideCloseButton);

  ui->gsoutData->setWindowTitle(tr("GS/DS Output"));
  ui->dockarea->addToolWindow(
      ui->gsoutData, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
                                                      ui->dockarea->areaOf(ui->vsoutData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->gsoutData, ToolWindowManager::HideCloseButton);

  ToolWindowManager::raiseToolWindow(ui->vsoutData);

  QMenu *headerMenu = new QMenu(this);

  QAction *resetColSel = new QAction(tr("Reset Selected Columns"), this);
  QAction *selPos = new QAction(tr("Select as Position"), this);
  QAction *selSec = new QAction(tr("Select as Secondary"), this);
  QAction *selSecAlpha = new QAction(tr("Select Alpha as Secondary"), this);

  headerMenu->addAction(resetColSel);
  headerMenu->addSeparator();
  headerMenu->addAction(selPos);
  headerMenu->addAction(selSec);
  headerMenu->addAction(selSecAlpha);

  QObject::connect(resetColSel, &QAction::triggered, [this]() {
    guessPositionColumn((BufferItemModel *)m_CurView->model());
    guessSecondaryColumn((BufferItemModel *)m_CurView->model());
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selPos, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setPosColumn(m_ContextColumn);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selSec, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setSecondaryColumn(m_ContextColumn, m_Config.solidShadeMode == eShade_Secondary, false);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selSecAlpha, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setSecondaryColumn(m_ContextColumn, m_Config.solidShadeMode == eShade_Secondary, true);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });

  ui->vsinData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->vsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->gsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);

  QObject::connect(
      ui->vsinData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->vsinData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->vsinData;
        m_ContextColumn = m_ModelVSIn->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelVSIn->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->vsinData->horizontalHeader()->mapToGlobal(pos));
      });
  QObject::connect(
      ui->vsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->vsoutData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->vsoutData;
        m_ContextColumn = m_ModelVSOut->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelVSOut->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->vsoutData->horizontalHeader()->mapToGlobal(pos));
      });
  QObject::connect(
      ui->gsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->gsoutData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->gsoutData;
        m_ContextColumn = m_ModelGSOut->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelGSOut->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->gsoutData->horizontalHeader()->mapToGlobal(pos));
      });

  QVBoxLayout *vertical = new QVBoxLayout(this);

  vertical->setSpacing(3);
  vertical->setContentsMargins(0, 0, 0, 0);

  vertical->addWidget(ui->meshToolbar);
  vertical->addWidget(ui->dockarea);

  QTimer *renderTimer = new QTimer(this);
  QObject::connect(renderTimer, &QTimer::timeout, this, &BufferViewer::render_timer);
  renderTimer->setSingleShot(false);
  renderTimer->setInterval(10);
  renderTimer->start();
}

BufferViewer::~BufferViewer()
{
  if(m_ModelVSIn->indices)
    m_ModelVSIn->indices->deref();

  for(auto vb : m_ModelVSIn->buffers)
    vb->deref();

  if(m_ModelVSOut->indices)
    m_ModelVSOut->indices->deref();

  for(auto vb : m_ModelVSOut->buffers)
    vb->deref();

  for(auto vb : m_ModelGSOut->buffers)
    vb->deref();

  delete m_Arcball;
  delete m_Flycam;

  if(m_MeshView)
    m_Ctx.windowClosed(this);

  m_Ctx.RemoveLogViewer(this);
  delete ui;
}

void BufferViewer::OnLogfileLoaded()
{
  Reset();

  if(!m_MeshView)
    return;

  WId renderID = ui->render->winId();

  m_Ctx.Renderer().BlockInvoke([renderID, this](IReplayRenderer *r) {
    m_Output = r->CreateOutput(m_Ctx.m_CurWinSystem, m_Ctx.FillWindowingData(renderID),
                               eOutputType_MeshDisplay);

    ui->render->setOutput(m_Output);

    OutputConfig c = {eOutputType_MeshDisplay};
    m_Output->SetOutputConfig(c);

    RT_UpdateAndDisplay(r);
  });
}

void BufferViewer::OnLogfileClosed()
{
  Reset();
}

void BufferViewer::OnEventChanged(uint32_t eventID)
{
  int vsinHoriz = ui->vsinData->horizontalScrollBar()->value();
  int vsoutHoriz = ui->vsoutData->horizontalScrollBar()->value();
  int gsoutHoriz = ui->gsoutData->horizontalScrollBar()->value();

  QString highlightNames[6] = {
      m_ModelVSIn->posName(),        m_ModelVSIn->secondaryName(), m_ModelVSOut->posName(),
      m_ModelVSOut->secondaryName(), m_ModelGSOut->posName(),      m_ModelGSOut->secondaryName(),
  };

  if(m_MeshView)
  {
    ClearModels();

    CalcColumnWidth();

    ClearModels();
  }

  EnableCameraGuessControls();

  m_ModelVSIn->curInstance = m_Config.curInstance;
  m_ModelVSOut->curInstance = m_Config.curInstance;
  m_ModelGSOut->curInstance = m_Config.curInstance;

  m_ModelVSIn->beginReset();
  m_ModelVSOut->beginReset();
  m_ModelGSOut->beginReset();

  const FetchDrawcall *draw = m_Ctx.CurDrawcall();

  m_ModelVSIn->baseVertex = draw ? draw->baseVertex : 0;
  m_ModelVSOut->baseVertex = draw ? draw->baseVertex : 0;
  m_ModelGSOut->baseVertex = draw ? draw->baseVertex : 0;

  ui->instance->setEnabled(draw && draw->numInstances > 1);
  if(!ui->instance->isEnabled())
    ui->instance->setValue(0);

  if(draw)
    ui->instance->setMaximum(qMax(0, int(draw->numInstances) - 1));

  if(m_MeshView)
  {
    configureMeshColumns();

    if(m_ModelVSIn->posColumn() == -1 || highlightNames[0] != m_ModelVSIn->posName())
      guessPositionColumn(m_ModelVSIn);
    if(m_ModelVSIn->secondaryColumn() == -1 || highlightNames[1] != m_ModelVSIn->secondaryName())
      guessSecondaryColumn(m_ModelVSIn);
    if(m_ModelVSOut->posColumn() == -1 || highlightNames[2] != m_ModelVSOut->posName())
      guessSecondaryColumn(m_ModelVSOut);
    if(m_ModelVSOut->secondaryColumn() == -1 || highlightNames[3] != m_ModelVSOut->secondaryName())
      guessPositionColumn(m_ModelVSOut);
    if(m_ModelGSOut->posColumn() == -1 || highlightNames[4] != m_ModelGSOut->posName())
      guessPositionColumn(m_ModelGSOut);
    if(m_ModelGSOut->secondaryColumn() == -1 || highlightNames[5] != m_ModelGSOut->secondaryName())<commit_msg>Only VSIn stage applies baseVertex offset.<commit_after>/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 <NAME>
 *
 * 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 "BufferViewer.h"
#include <float.h>
#include <QDoubleSpinBox>
#include <QFontDatabase>
#include <QMenu>
#include <QMouseEvent>
#include <QScrollBar>
#include <QTimer>
#include <QtMath>
#include "Code/Resources.h"
#include "Windows/ShaderViewer.h"
#include "ui_BufferViewer.h"

class CameraWrapper
{
public:
  virtual ~CameraWrapper() {}
  virtual bool Update(QRect winSize) = 0;
  virtual Camera *camera() = 0;

  virtual void MouseWheel(QWheelEvent *e) = 0;

  virtual void MouseClick(QMouseEvent *e) { m_DragStartPos = e->pos(); }
  virtual void MouseMove(QMouseEvent *e)
  {
    if(e->buttons() & Qt::LeftButton)
    {
      if(m_DragStartPos.x() < 0)
      {
        m_DragStartPos = e->pos();
      }

      m_DragStartPos = e->pos();
    }
    else
    {
      m_DragStartPos = QPoint(-1, -1);
    }
  }

  virtual void KeyUp(QKeyEvent *e)
  {
    if(e->key() == Qt::Key_A || e->key() == Qt::Key_D)
      setMove(Direction::Horiz, 0);
    if(e->key() == Qt::Key_Q || e->key() == Qt::Key_E)
      setMove(Direction::Vert, 0);
    if(e->key() == Qt::Key_W || e->key() == Qt::Key_S)
      setMove(Direction::Fwd, 0);

    if(e->modifiers() && Qt::ShiftModifier)
      m_CurrentSpeed = 3.0f;
    else
      m_CurrentSpeed = 1.0f;
  }

  virtual void KeyDown(QKeyEvent *e)
  {
    if(e->key() == Qt::Key_W)
      setMove(Direction::Fwd, 1);
    if(e->key() == Qt::Key_S)
      setMove(Direction::Fwd, -1);
    if(e->key() == Qt::Key_Q)
      setMove(Direction::Vert, 1);
    if(e->key() == Qt::Key_E)
      setMove(Direction::Vert, -1);
    if(e->key() == Qt::Key_D)
      setMove(Direction::Horiz, 1);
    if(e->key() == Qt::Key_A)
      setMove(Direction::Horiz, -1);

    if(e->modifiers() && Qt::ShiftModifier)
      m_CurrentSpeed = 3.0f;
    else
      m_CurrentSpeed = 1.0f;
  }

  float SpeedMultiplier = 0.05f;

protected:
  enum class Direction
  {
    Fwd,
    Horiz,
    Vert,
    Num
  };

  int move(Direction dir) { return m_CurrentMove[(int)dir]; }
  float currentSpeed() { return m_CurrentSpeed * SpeedMultiplier; }
  QPoint dragStartPos() { return m_DragStartPos; }
private:
  float m_CurrentSpeed = 1.0f;
  int m_CurrentMove[(int)Direction::Num] = {0, 0, 0};

  void setMove(Direction dir, int val) { m_CurrentMove[(int)dir] = val; }
  QPoint m_DragStartPos = QPoint(-1, -1);
};

class ArcballWrapper : public CameraWrapper
{
public:
  ArcballWrapper() { m_Cam = Camera_InitArcball(); }
  virtual ~ArcballWrapper() { Camera_Shutdown(m_Cam); }
  Camera *camera() override { return m_Cam; }
  void Reset(FloatVector pos, float dist)
  {
    Camera_ResetArcball(m_Cam);

    setLookAtPos(pos);
    SetDistance(dist);
  }

  void SetDistance(float dist)
  {
    m_Distance = qAbs(dist);
    Camera_SetArcballDistance(m_Cam, m_Distance);
  }

  bool Update(QRect size) override
  {
    m_WinSize = size;
    return false;
  }

  void MouseWheel(QWheelEvent *e) override
  {
    float mod = (1.0f - e->delta() / 2500.0f);

    SetDistance(qMax(1e-6f, m_Distance * mod));
  }

  void MouseMove(QMouseEvent *e) override
  {
    if(dragStartPos().x() > 0)
    {
      if(e->buttons() == Qt::MiddleButton ||
         (e->buttons() == Qt::LeftButton && e->modifiers() & Qt::AltModifier))
      {
        float xdelta = (float)(e->pos().x() - dragStartPos().x()) / 300.0f;
        float ydelta = (float)(e->pos().y() - dragStartPos().y()) / 300.0f;

        xdelta *= qMax(1.0f, m_Distance);
        ydelta *= qMax(1.0f, m_Distance);

        FloatVector pos, fwd, right, up;
        Camera_GetBasis(m_Cam, &pos, &fwd, &right, &up);

        m_LookAt.x -= right.x * xdelta;
        m_LookAt.y -= right.y * xdelta;
        m_LookAt.z -= right.z * xdelta;

        m_LookAt.x += up.x * ydelta;
        m_LookAt.y += up.y * ydelta;
        m_LookAt.z += up.z * ydelta;

        Camera_SetPosition(m_Cam, m_LookAt.x, m_LookAt.y, m_LookAt.z);
      }
      else if(e->buttons() == Qt::LeftButton)
      {
        RotateArcball(dragStartPos(), e->pos());
      }
    }

    CameraWrapper::MouseMove(e);
  }

  FloatVector lookAtPos() { return m_LookAt; }
  void setLookAtPos(const FloatVector &v)
  {
    m_LookAt = v;
    Camera_SetPosition(m_Cam, v.x, v.y, v.z);
  }

private:
  Camera *m_Cam;

  QRect m_WinSize;

  float m_Distance = 10.0f;
  FloatVector m_LookAt;

  void RotateArcball(QPoint from, QPoint to)
  {
    float ax = ((float)from.x() / (float)m_WinSize.width()) * 2.0f - 1.0f;
    float ay = ((float)from.y() / (float)m_WinSize.height()) * 2.0f - 1.0f;
    float bx = ((float)to.x() / (float)m_WinSize.width()) * 2.0f - 1.0f;
    float by = ((float)to.y() / (float)m_WinSize.height()) * 2.0f - 1.0f;

    // this isn't a 'true arcball' but it handles extreme aspect ratios
    // better. We basically 'centre' around the from point always being
    // 0,0 (straight out of the screen) as if you're always dragging
    // the arcball from the middle, and just use the relative movement
    int minDimension = qMin(m_WinSize.width(), m_WinSize.height());

    ax = ay = 0;
    bx = ((float)(to.x() - from.x()) / (float)minDimension) * 2.0f;
    by = ((float)(to.y() - from.y()) / (float)minDimension) * 2.0f;

    ay = -ay;
    by = -by;

    Camera_RotateArcball(m_Cam, ax, ay, bx, by);
  }
};

class FlycamWrapper : public CameraWrapper
{
public:
  FlycamWrapper() { m_Cam = Camera_InitFPSLook(); }
  virtual ~FlycamWrapper() { Camera_Shutdown(m_Cam); }
  Camera *camera() override { return m_Cam; }
  void Reset(FloatVector pos)
  {
    m_Position = pos;
    m_Rotation = FloatVector();

    Camera_SetPosition(m_Cam, m_Position.x, m_Position.y, m_Position.z);
    Camera_SetFPSRotation(m_Cam, m_Rotation.x, m_Rotation.y, m_Rotation.z);
  }

  bool Update(QRect size) override
  {
    FloatVector pos, fwd, right, up;
    Camera_GetBasis(m_Cam, &pos, &fwd, &right, &up);

    float speed = currentSpeed();

    int horizMove = move(CameraWrapper::Direction::Horiz);
    if(horizMove)
    {
      m_Position.x += right.x * speed * (float)horizMove;
      m_Position.y += right.y * speed * (float)horizMove;
      m_Position.z += right.z * speed * (float)horizMove;
    }

    int vertMove = move(CameraWrapper::Direction::Vert);
    if(vertMove)
    {
      // this makes less intuitive sense, instead go 'absolute' up
      // m_Position.x += up.x * speed * (float)vertMove;
      // m_Position.y += up.y * speed * (float)vertMove;
      // m_Position.z += up.z * speed * (float)vertMove;

      m_Position.y += speed * (float)vertMove;
    }

    int fwdMove = move(CameraWrapper::Direction::Fwd);
    if(fwdMove)
    {
      m_Position.x += fwd.x * speed * (float)fwdMove;
      m_Position.y += fwd.y * speed * (float)fwdMove;
      m_Position.z += fwd.z * speed * (float)fwdMove;
    }

    if(horizMove || vertMove || fwdMove)
    {
      Camera_SetPosition(m_Cam, m_Position.x, m_Position.y, m_Position.z);
      return true;
    }

    return false;
  }

  void MouseWheel(QWheelEvent *e) override {}
  void MouseMove(QMouseEvent *e) override
  {
    if(dragStartPos().x() > 0 && e->buttons() == Qt::LeftButton)
    {
      m_Rotation.y -= (float)(e->pos().x() - dragStartPos().x()) / 300.0f;
      m_Rotation.x -= (float)(e->pos().y() - dragStartPos().y()) / 300.0f;

      Camera_SetFPSRotation(m_Cam, m_Rotation.x, m_Rotation.y, m_Rotation.z);
    }

    CameraWrapper::MouseMove(e);
  }

private:
  Camera *m_Cam;

  FloatVector m_Position, m_Rotation;
};

struct BufferData
{
  BufferData()
  {
    refcount.store(1);
    data = end = NULL;
    stride = 0;
  }

  void ref() { refcount.ref(); }
  void deref()
  {
    bool alive = refcount.deref();

    if(!alive)
    {
      delete[] data;
      delete this;
    }
  }

  QAtomicInteger<uint32_t> refcount;
  byte *data;
  byte *end;
  size_t stride;
};

uint32_t CalcIndex(BufferData *data, uint32_t vertID, int32_t baseVertex)
{
  byte *idxData = data->data + vertID * sizeof(uint32_t);
  if(idxData + sizeof(uint32_t) > data->end)
    return ~0U;

  uint32_t idx = *(uint32_t *)idxData;

  // apply base vertex but clamp to 0 if subtracting
  if(baseVertex < 0)
  {
    uint32_t subtract = (uint32_t)(-baseVertex);

    if(idx < subtract)
      idx = 0;
    else
      idx -= subtract;
  }
  else if(baseVertex > 0)
  {
    idx += (uint32_t)baseVertex;
  }

  return idx;
}

class BufferItemModel : public QAbstractItemModel
{
public:
  BufferItemModel(RDTableView *v, QObject *parent) : QAbstractItemModel(parent)
  {
    view = v;
    view->setModel(this);
  }
  void beginReset() { emit beginResetModel(); }
  void endReset()
  {
    cacheColumns();
    m_ColumnCount = columnLookup.count() + reservedColumnCount();
    emit endResetModel();
  }
  QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
  {
    if(row < 0 || row >= rowCount())
      return QModelIndex();

    return createIndex(row, column);
  }

  QModelIndex parent(const QModelIndex &index) const override { return QModelIndex(); }
  int rowCount(const QModelIndex &parent = QModelIndex()) const override { return numRows; }
  int columnCount(const QModelIndex &parent = QModelIndex()) const override
  {
    return m_ColumnCount;
  }
  Qt::ItemFlags flags(const QModelIndex &index) const override
  {
    if(!index.isValid())
      return 0;

    return QAbstractItemModel::flags(index);
  }

  QVariant headerData(int section, Qt::Orientation orientation, int role) const override
  {
    if(section < m_ColumnCount && orientation == Qt::Horizontal && role == Qt::DisplayRole)
    {
      if(section == 0 && meshView)
      {
        return "VTX";
      }
      else if(section == 1 && meshView)
      {
        return "IDX";
      }
      else
      {
        const FormatElement &el = elementForColumn(section);

        if(el.format.compCount == 1)
          return el.name;

        QChar comps[] = {'x', 'y', 'z', 'w'};

        return QString("%1.%2").arg(el.name).arg(comps[componentForIndex(section)]);
      }
    }

    return QVariant();
  }

  QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
  {
    if(index.isValid())
    {
      if(role == Qt::SizeHintRole)
      {
        QStyleOptionViewItem opt = view->viewOptions();
        opt.features |= QStyleOptionViewItem::HasDisplay;

        // pad these columns to allow for sufficiently wide data
        if(index.column() < 2 && meshView)
          opt.text = "999999";
        else
          opt.text = data(index).toString();
        opt.styleObject = NULL;

        QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
        return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), opt.widget);
      }

      uint32_t row = index.row();
      int col = index.column();

      if((role == Qt::BackgroundRole || role == Qt::ForegroundRole) && col >= reservedColumnCount())
      {
        int elIdx = columnLookup[col - reservedColumnCount()];
        int compIdx = componentForIndex(col);
        if(elIdx == positionEl)
        {
          if(role == Qt::ForegroundRole)
            return QBrush(Qt::black);

          if(compIdx != 3 || !meshInput)
          {
            // C# SkyBlue
            return QBrush(QColor::fromRgb(135, 206, 235));
          }
          else
          {
            // C# LightCyan
            return QBrush(QColor::fromRgb(224, 255, 255));
          }
        }
        else if(secondaryEnabled && elIdx == secondaryEl)
        {
          if(role == Qt::ForegroundRole)
            return QBrush(Qt::black);

          if((secondaryElAlpha && compIdx == 3) || (!secondaryElAlpha && compIdx != 3))
          {
            // C# LightGreen
            return QBrush(QColor::fromRgb(144, 238, 144));
          }
          else
          {
            return QBrush(QColor::fromRgb(200, 238, 200));
          }
        }
      }

      if(role == Qt::DisplayRole)
      {
        if(col >= 0 && col < m_ColumnCount && row < numRows)
        {
          if(col == 0 && meshView)
            return row;

          uint32_t idx = row;

          if(indices && indices->data)
          {
            idx = CalcIndex(indices, row, baseVertex);

            if(idx == ~0U)
              return QVariant();
          }

          if(col == 1 && meshView)
            return idx;

          const FormatElement &el = elementForColumn(col);

          uint32_t instIdx = 0;
          if(el.instancerate > 0)
            instIdx = curInstance / el.instancerate;

          if(el.buffer < buffers.size())
          {
            const byte *data = buffers[el.buffer]->data;
            const byte *end = buffers[el.buffer]->end;

            if(!el.perinstance)
              data += buffers[el.buffer]->stride * idx;
            else
              data += buffers[el.buffer]->stride * instIdx;

            data += el.offset;

            // only slightly wasteful, we need to fetch all variants together
            // since some formats are packed and can't be read individually
            QVariantList list = el.GetVariants(data, end);

            int comp = componentForIndex(col);

            if(comp < list.count())
            {
              QVariant &v = list[comp];

              QString ret;

              QMetaType::Type vt = (QMetaType::Type)v.type();

              if(vt == QMetaType::Double)
              {
                double d = v.toDouble();
                // pad with space on left if sign is missing, to better align
                if(d < 0.0)
                  ret = Formatter::Format(d);
                else if(d > 0.0)
                  ret = " " + Formatter::Format(d);
                else if(qIsNaN(d))
                  ret = " NaN";
                else
                  // force negative and positive 0 together
                  ret = " " + Formatter::Format(0.0);
              }
              else if(vt == QMetaType::Float)
              {
                float f = v.toFloat();
                // pad with space on left if sign is missing, to better align
                if(f < 0.0)
                  ret = Formatter::Format(f);
                else if(f > 0.0)
                  ret = " " + Formatter::Format(f);
                else if(qIsNaN(f))
                  ret = " NaN";
                else
                  // force negative and positive 0 together
                  ret = " " + Formatter::Format(0.0);
              }
              else if(vt == QMetaType::UInt || vt == QMetaType::UShort || vt == QMetaType::UChar)
              {
                ret = Formatter::Format(v.toUInt(), el.hex);
              }
              else if(vt == QMetaType::Int || vt == QMetaType::Short || vt == QMetaType::SChar)
              {
                int i = v.toInt();
                if(i > 0)
                  ret = " " + Formatter::Format(i);
                else
                  ret = Formatter::Format(i);
              }
              else
                ret = v.toString();

              return ret;
            }
          }
        }
      }
    }

    return QVariant();
  }

  RDTableView *view = NULL;

  int32_t baseVertex = 0;
  uint32_t curInstance = 0;
  uint32_t numRows = 0;
  bool meshView = true;
  bool meshInput = false;
  BufferData *indices = NULL;
  QList<FormatElement> columns;
  QList<BufferData *> buffers;

  void setPosColumn(int pos)
  {
    QVector<int> roles = {Qt::BackgroundRole, Qt::ForegroundRole};

    if(positionEl != pos)
    {
      if(positionEl >= 0)
        emit dataChanged(index(0, firstColumnForElement(positionEl)),
                         index(rowCount() - 1, lastColumnForElement(positionEl)), roles);

      if(pos >= 0)
        emit dataChanged(index(0, firstColumnForElement(pos)),
                         index(rowCount() - 1, lastColumnForElement(pos)), roles);
    }

    positionEl = pos;
  }

  int posColumn() { return positionEl; }
  QString posName()
  {
    if(positionEl >= 0 && positionEl < columns.count())
      return columns[positionEl].name;

    return "";
  }

  void setSecondaryColumn(int sec, bool secEnabled, bool secAlpha)
  {
    QVector<int> roles = {Qt::BackgroundRole, Qt::ForegroundRole};

    if(secondaryEl != sec || secondaryElAlpha != secAlpha || secondaryEnabled != secEnabled)
    {
      if(secondaryEl >= 0 && secondaryEl != sec)
        emit dataChanged(index(0, firstColumnForElement(secondaryEl)),
                         index(rowCount() - 1, lastColumnForElement(secondaryEl)), roles);

      if(sec >= 0)
        emit dataChanged(index(0, firstColumnForElement(sec)),
                         index(rowCount() - 1, lastColumnForElement(sec)), roles);
    }

    secondaryEl = sec;
    secondaryElAlpha = secAlpha;
    secondaryEnabled = secEnabled;
  }

  int secondaryColumn() { return secondaryEl; }
  bool secondaryAlpha() { return secondaryElAlpha; }
  QString secondaryName()
  {
    if(secondaryEl >= 0 && secondaryEl < columns.count())
      return columns[secondaryEl].name;

    return "";
  }
  int elementIndexForColumn(int col) const
  {
    if(col < reservedColumnCount())
      return -1;

    return columnLookup[col - reservedColumnCount()];
  }

  const FormatElement &elementForColumn(int col) const
  {
    return columns[columnLookup[col - reservedColumnCount()]];
  }

private:
  // maps from column number (0-based from data, so excluding VTX/IDX columns)
  // to the column element in the columns list, and lists its component.
  //
  // So a float4, float3, int set of columns would be:
  // { 0, 0, 0, 0, 1, 1, 1, 2 };
  // { 0, 1, 2, 3, 0, 1, 2, 0 };
  QVector<int> columnLookup;
  QVector<int> componentLookup;
  int m_ColumnCount = 0;

  int positionEl = -1;
  int secondaryEl = -1;
  bool secondaryElAlpha = false;
  bool secondaryEnabled = false;

  int reservedColumnCount() const { return (meshView ? 2 : 0); }
  int componentForIndex(int col) const { return componentLookup[col - reservedColumnCount()]; }
  int firstColumnForElement(int el) const
  {
    for(int i = 0; i < columnLookup.count(); i++)
    {
      if(columnLookup[i] == el)
        return reservedColumnCount() + i;
    }

    return 0;
  }

  int lastColumnForElement(int el) const
  {
    for(int i = columnLookup.count() - 1; i >= 0; i--)
    {
      if(columnLookup[i] == el)
        return reservedColumnCount() + i;
    }

    return columnCount() - 1;
  }

  void cacheColumns()
  {
    columnLookup.clear();
    columnLookup.reserve(columns.count() * 4);
    componentLookup.clear();
    componentLookup.reserve(columns.count() * 4);

    for(int i = 0; i < columns.count(); i++)
    {
      FormatElement &fmt = columns[i];

      uint32_t compCount;

      switch(fmt.format.specialFormat)
      {
        case eSpecial_BC6:
        case eSpecial_ETC2:
        case eSpecial_R11G11B10:
        case eSpecial_R5G6B5:
        case eSpecial_R9G9B9E5: compCount = 3; break;
        case eSpecial_BC1:
        case eSpecial_BC7:
        case eSpecial_BC3:
        case eSpecial_BC2:
        case eSpecial_R10G10B10A2:
        case eSpecial_R5G5B5A1:
        case eSpecial_R4G4B4A4:
        case eSpecial_ASTC: compCount = 4; break;
        case eSpecial_BC5:
        case eSpecial_R4G4:
        case eSpecial_D16S8:
        case eSpecial_D24S8:
        case eSpecial_D32S8: compCount = 2; break;
        case eSpecial_BC4:
        case eSpecial_S8: compCount = 1; break;
        case eSpecial_YUV:
        case eSpecial_EAC:
        default: compCount = fmt.format.compCount;
      }

      for(uint32_t c = 0; c < compCount; c++)
      {
        columnLookup.push_back(i);
        componentLookup.push_back((int)c);
      }
    }
  }
};

struct CachedElData
{
  const FormatElement *el = NULL;

  const byte *data = NULL;
  const byte *end = NULL;

  size_t stride;
  int byteSize;
  uint32_t instIdx = 0;

  QByteArray nulls;
};

void CacheDataForIteration(QVector<CachedElData> &cache, const QList<FormatElement> &columns,
                           const QList<BufferData *> buffers, uint32_t inst)
{
  cache.reserve(columns.count());

  for(int col = 0; col < columns.count(); col++)
  {
    const FormatElement &el = columns[col];

    CachedElData d;

    d.el = &el;

    d.byteSize = el.byteSize();
    d.nulls = QByteArray(d.byteSize, '\0');

    if(el.instancerate > 0)
      d.instIdx = inst / el.instancerate;

    if(el.buffer < buffers.size())
    {
      d.data = buffers[el.buffer]->data;
      d.end = buffers[el.buffer]->end;

      d.stride = buffers[el.buffer]->stride;

      d.data += el.offset;

      if(el.perinstance)
        d.data += d.stride * d.instIdx;
    }

    cache.push_back(d);
  }
}

BufferViewer::BufferViewer(CaptureContext &ctx, bool meshview, QWidget *parent)
    : QFrame(parent), ui(new Ui::BufferViewer), m_Ctx(ctx)
{
  ui->setupUi(this);

  m_ModelVSIn = new BufferItemModel(ui->vsinData, this);
  m_ModelVSOut = new BufferItemModel(ui->vsoutData, this);
  m_ModelGSOut = new BufferItemModel(ui->gsoutData, this);

  m_Flycam = new FlycamWrapper();
  m_Arcball = new ArcballWrapper();
  m_CurrentCamera = m_Arcball;

  m_Output = NULL;

  memset(&m_Config, 0, sizeof(m_Config));
  m_Config.type = eMeshDataStage_VSIn;
  m_Config.wireframeDraw = true;

  ui->outputTabs->setCurrentIndex(0);
  m_CurStage = eMeshDataStage_VSIn;

  ui->vsinData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
  ui->vsoutData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
  ui->gsoutData->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));

  m_ModelVSIn->meshView = m_ModelVSOut->meshView = m_ModelGSOut->meshView = m_MeshView = meshview;

  m_ModelVSIn->meshInput = true;

  if(meshview)
    SetupMeshView();
  else
    SetupRawView();

  QMenu *exportMenu = new QMenu(this);

  QAction *csv = new QAction(tr("Export to &CSV"), this);
  csv->setIcon(Icons::save());
  exportMenu->addAction(csv);
  QAction *bytes = new QAction(tr("Export to &Bytes"), this);
  bytes->setIcon(Icons::save());
  exportMenu->addAction(bytes);

  QAction *debug = new QAction(tr("&Debug this Vertex"), this);
  debug->setIcon(Icons::wrench());

  ui->exportDrop->setMenu(exportMenu);

  QObject::connect(csv, &QAction::triggered, [this] { exportData(BufferExport(BufferExport::CSV)); });
  QObject::connect(bytes, &QAction::triggered,
                   [this] { exportData(BufferExport(BufferExport::RawBytes)); });
  QObject::connect(debug, &QAction::triggered, this, &BufferViewer::debugVertex);

  QObject::connect(ui->exportDrop, &QToolButton::clicked,
                   [this] { exportData(BufferExport(BufferExport::CSV)); });

  ui->vsinData->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->vsoutData->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->gsoutData->setContextMenuPolicy(Qt::CustomContextMenu);

  QMenu *vsinMenu = new QMenu(this);

  QObject::connect(ui->vsinData, &RDTableView::customContextMenuRequested,
                   [this, vsinMenu, debug, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->vsinData;

                     vsinMenu->clear();

                     if(m_MeshView)
                     {
                       vsinMenu->addAction(debug);
                       vsinMenu->addSeparator();
                     }

                     vsinMenu->addAction(csv);
                     vsinMenu->addAction(bytes);

                     vsinMenu->popup(ui->vsinData->viewport()->mapToGlobal(pos));
                   });

  QMenu *vsoutMenu = new QMenu(this);

  QObject::connect(ui->vsoutData, &RDTableView::customContextMenuRequested,
                   [this, vsoutMenu, debug, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->vsoutData;

                     vsoutMenu->clear();

                     if(m_MeshView)
                     {
                       vsoutMenu->addAction(debug);
                       vsoutMenu->addSeparator();
                     }

                     vsoutMenu->addAction(csv);
                     vsoutMenu->addAction(bytes);

                     vsoutMenu->popup(ui->vsoutData->viewport()->mapToGlobal(pos));
                   });

  QMenu *gsoutMenu = new QMenu(this);

  QObject::connect(ui->gsoutData, &RDTableView::customContextMenuRequested,
                   [this, gsoutMenu, csv, bytes](const QPoint &pos) {
                     m_CurView = ui->gsoutData;

                     gsoutMenu->clear();

                     gsoutMenu->addAction(csv);
                     gsoutMenu->addAction(bytes);

                     gsoutMenu->popup(ui->gsoutData->viewport()->mapToGlobal(pos));
                   });

  ui->dockarea->setAllowFloatingWindow(false);
  ui->dockarea->setRubberBandLineWidth(50);

  ui->controlType->addItems({tr("Arcball"), tr("WASD")});
  ui->controlType->adjustSize();

  ui->drawRange->addItems({tr("Only this draw"), tr("Show previous instances"),
                           tr("Show all instances"), tr("Show whole pass")});
  ui->drawRange->adjustSize();
  ui->drawRange->setCurrentIndex(0);

  ui->solidShading->addItems({tr("None"), tr("Solid Colour"), tr("Flat Shaded"), tr("Secondary")});
  ui->solidShading->adjustSize();
  ui->solidShading->setCurrentIndex(0);

  // wireframe only available on solid shaded options
  ui->wireframeRender->setEnabled(false);

  ui->fovGuess->setValue(90.0);

  on_controlType_currentIndexChanged(0);

  QObject::connect(ui->vsinData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);
  QObject::connect(ui->vsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);
  QObject::connect(ui->gsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                   &BufferViewer::data_selected);

  QObject::connect(ui->vsinData, &RDTableView::clicked, [this]() { m_CurView = ui->vsinData; });
  QObject::connect(ui->vsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->vsoutData; });
  QObject::connect(ui->gsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->gsoutData; });

  QObject::connect(ui->vsinData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);
  QObject::connect(ui->vsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);
  QObject::connect(ui->gsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
                   &BufferViewer::data_scrolled);

  QObject::connect(ui->fovGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->aspectGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->nearGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->farGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
                   &BufferViewer::camGuess_changed);
  QObject::connect(ui->matrixType, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged),
                   [this](int) { camGuess_changed(0.0); });

  Reset();

  m_Ctx.AddLogViewer(this);
}

void BufferViewer::SetupRawView()
{
  ui->formatSpecifier->setVisible(true);
  ui->outputTabs->setVisible(false);
  ui->vsoutData->setVisible(false);
  ui->gsoutData->setVisible(false);

  // hide buttons we don't want in the toolbar
  ui->syncViews->setVisible(false);
  ui->offsetLine->setVisible(false);
  ui->instanceLabel->setVisible(false);
  ui->instance->setVisible(false);
  ui->rowOffsetLabel->setVisible(false);
  ui->rowOffset->setVisible(false);

  ui->vsinData->setWindowTitle(tr("Buffer Contents"));
  ui->dockarea->addToolWindow(ui->vsinData, ToolWindowManager::EmptySpace);
  ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);

  ui->formatSpecifier->setWindowTitle(tr("Buffer Format"));
  ui->dockarea->addToolWindow(ui->formatSpecifier, ToolWindowManager::AreaReference(
                                                       ToolWindowManager::BottomOf,
                                                       ui->dockarea->areaOf(ui->vsinData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->formatSpecifier, ToolWindowManager::HideCloseButton);

  QObject::connect(ui->formatSpecifier, &BufferFormatSpecifier::processFormat, this,
                   &BufferViewer::processFormat);

  QVBoxLayout *vertical = new QVBoxLayout(this);

  vertical->setSpacing(3);
  vertical->setContentsMargins(0, 0, 0, 0);

  vertical->addWidget(ui->meshToolbar);
  vertical->addWidget(ui->dockarea);
}

void BufferViewer::SetupMeshView()
{
  setWindowTitle(tr("Mesh Output"));

  ui->formatSpecifier->setVisible(false);
  ui->cameraControlsGroup->setVisible(false);

  ui->outputTabs->setWindowTitle(tr("Preview"));
  ui->dockarea->addToolWindow(ui->outputTabs, ToolWindowManager::EmptySpace);
  ui->dockarea->setToolWindowProperties(ui->outputTabs, ToolWindowManager::HideCloseButton);

  ui->vsinData->setWindowTitle(tr("VS Input"));
  ui->dockarea->addToolWindow(
      ui->vsinData, ToolWindowManager::AreaReference(ToolWindowManager::TopOf,
                                                     ui->dockarea->areaOf(ui->outputTabs), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);

  ui->vsoutData->setWindowTitle(tr("VS Output"));
  ui->dockarea->addToolWindow(
      ui->vsoutData, ToolWindowManager::AreaReference(ToolWindowManager::RightOf,
                                                      ui->dockarea->areaOf(ui->vsinData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->vsoutData, ToolWindowManager::HideCloseButton);

  ui->gsoutData->setWindowTitle(tr("GS/DS Output"));
  ui->dockarea->addToolWindow(
      ui->gsoutData, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
                                                      ui->dockarea->areaOf(ui->vsoutData), 0.5f));
  ui->dockarea->setToolWindowProperties(ui->gsoutData, ToolWindowManager::HideCloseButton);

  ToolWindowManager::raiseToolWindow(ui->vsoutData);

  QMenu *headerMenu = new QMenu(this);

  QAction *resetColSel = new QAction(tr("Reset Selected Columns"), this);
  QAction *selPos = new QAction(tr("Select as Position"), this);
  QAction *selSec = new QAction(tr("Select as Secondary"), this);
  QAction *selSecAlpha = new QAction(tr("Select Alpha as Secondary"), this);

  headerMenu->addAction(resetColSel);
  headerMenu->addSeparator();
  headerMenu->addAction(selPos);
  headerMenu->addAction(selSec);
  headerMenu->addAction(selSecAlpha);

  QObject::connect(resetColSel, &QAction::triggered, [this]() {
    guessPositionColumn((BufferItemModel *)m_CurView->model());
    guessSecondaryColumn((BufferItemModel *)m_CurView->model());
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selPos, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setPosColumn(m_ContextColumn);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selSec, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setSecondaryColumn(m_ContextColumn, m_Config.solidShadeMode == eShade_Secondary, false);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });
  QObject::connect(selSecAlpha, &QAction::triggered, [this]() {
    BufferItemModel *model = (BufferItemModel *)m_CurView->model();

    model->setSecondaryColumn(m_ContextColumn, m_Config.solidShadeMode == eShade_Secondary, true);
    updatePreviewColumns();
    INVOKE_MEMFN(RT_UpdateAndDisplay);
  });

  ui->vsinData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->vsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
  ui->gsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);

  QObject::connect(
      ui->vsinData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->vsinData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->vsinData;
        m_ContextColumn = m_ModelVSIn->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelVSIn->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->vsinData->horizontalHeader()->mapToGlobal(pos));
      });
  QObject::connect(
      ui->vsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->vsoutData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->vsoutData;
        m_ContextColumn = m_ModelVSOut->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelVSOut->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->vsoutData->horizontalHeader()->mapToGlobal(pos));
      });
  QObject::connect(
      ui->gsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
      [this, headerMenu, selSecAlpha](const QPoint &pos) {
        int col = ui->gsoutData->horizontalHeader()->logicalIndexAt(pos);

        if(col < 2)
          return;

        m_CurView = ui->gsoutData;
        m_ContextColumn = m_ModelGSOut->elementIndexForColumn(col);

        selSecAlpha->setEnabled(m_ModelGSOut->elementForColumn(col).format.compCount == 4);

        headerMenu->popup(ui->gsoutData->horizontalHeader()->mapToGlobal(pos));
      });

  QVBoxLayout *vertical = new QVBoxLayout(this);

  vertical->setSpacing(3);
  vertical->setContentsMargins(0, 0, 0, 0);

  vertical->addWidget(ui->meshToolbar);
  vertical->addWidget(ui->dockarea);

  QTimer *renderTimer = new QTimer(this);
  QObject::connect(renderTimer, &QTimer::timeout, this, &BufferViewer::render_timer);
  renderTimer->setSingleShot(false);
  renderTimer->setInterval(10);
  renderTimer->start();
}

BufferViewer::~BufferViewer()
{
  if(m_ModelVSIn->indices)
    m_ModelVSIn->indices->deref();

  for(auto vb : m_ModelVSIn->buffers)
    vb->deref();

  if(m_ModelVSOut->indices)
    m_ModelVSOut->indices->deref();

  for(auto vb : m_ModelVSOut->buffers)
    vb->deref();

  for(auto vb : m_ModelGSOut->buffers)
    vb->deref();

  delete m_Arcball;
  delete m_Flycam;

  if(m_MeshView)
    m_Ctx.windowClosed(this);

  m_Ctx.RemoveLogViewer(this);
  delete ui;
}

void BufferViewer::OnLogfileLoaded()
{
  Reset();

  if(!m_MeshView)
    return;

  WId renderID = ui->render->winId();

  m_Ctx.Renderer().BlockInvoke([renderID, this](IReplayRenderer *r) {
    m_Output = r->CreateOutput(m_Ctx.m_CurWinSystem, m_Ctx.FillWindowingData(renderID),
                               eOutputType_MeshDisplay);

    ui->render->setOutput(m_Output);

    OutputConfig c = {eOutputType_MeshDisplay};
    m_Output->SetOutputConfig(c);

    RT_UpdateAndDisplay(r);
  });
}

void BufferViewer::OnLogfileClosed()
{
  Reset();
}

void BufferViewer::OnEventChanged(uint32_t eventID)
{
  int vsinHoriz = ui->vsinData->horizontalScrollBar()->value();
  int vsoutHoriz = ui->vsoutData->horizontalScrollBar()->value();
  int gsoutHoriz = ui->gsoutData->horizontalScrollBar()->value();

  QString highlightNames[6] = {
      m_ModelVSIn->posName(),        m_ModelVSIn->secondaryName(), m_ModelVSOut->posName(),
      m_ModelVSOut->secondaryName(), m_ModelGSOut->posName(),      m_ModelGSOut->secondaryName(),
  };

  if(m_MeshView)
  {
    ClearModels();

    CalcColumnWidth();

    ClearModels();
  }

  EnableCameraGuessControls();

  m_ModelVSIn->curInstance = m_Config.curInstance;
  m_ModelVSOut->curInstance = m_Config.curInstance;
  m_ModelGSOut->curInstance = m_Config.curInstance;

  m_ModelVSIn->beginReset();
  m_ModelVSOut->beginReset();
  m_ModelGSOut->beginReset();

  const FetchDrawcall *draw = m_Ctx.CurDrawcall();

  m_ModelVSIn->baseVertex = draw ? draw->baseVertex : 0;

  ui->instance->setEnabled(draw && draw->numInstances > 1);
  if(!ui->instance->isEnabled())
    ui->instance->setValue(0);

  if(draw)
    ui->instance->setMaximum(qMax(0, int(draw->numInstances) - 1));

  if(m_MeshView)
  {
    configureMeshColumns();

    if(m_ModelVSIn->posColumn() == -1 || highlightNames[0] != m_ModelVSIn->posName())
      guessPositionColumn(m_ModelVSIn);
    if(m_ModelVSIn->secondaryColumn() == -1 || highlightNames[1] != m_ModelVSIn->secondaryName())
      guessSecondaryColumn(m_ModelVSIn);
    if(m_ModelVSOut->posColumn() == -1 || highlightNames[2] != m_ModelVSOut->posName())
      guessSecondaryColumn(m_ModelVSOut);
    if(m_ModelVSOut->secondaryColumn() == -1 || highlightNames[3] != m_ModelVSOut->secondaryName())
      guessPositionColumn(m_ModelVSOut);
    if(m_ModelGSOut->posColumn() == -1 || highlightNames[4] != m_ModelGSOut->posName())
      guessPositionColumn(m_ModelGSOut);
    if(m_ModelGSOut->secondaryColumn() == -1 || highlightNames[5] != m_ModelGSOut->secondaryName())
{'fraction_non_alphanumeric': 0.14282749406560907, 'fraction_numerical': 0.010039822033128818, 'mean_word_length': 2.908044811679424, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 847, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7539558', 'n_tokens_mistral': 25595, 'n_tokens_neox': 24359, 'n_words': 4712}