/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2021 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 "TextureViewer.h"
#include <float.h>
#include <math.h>
#include <QClipboard>
#include <QColorDialog>
#include <QFileSystemWatcher>
#include <QFontDatabase>
#include <QItemDelegate>
#include <QJsonDocument>
#include <QMenu>
#include <QPainter>
#include <QPointer>
#include <QStyledItemDelegate>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"
#include "Dialogs/TextureSaveDialog.h"
#include "Widgets/Extended/RDHeaderView.h"
#include "Widgets/ResourcePreview.h"
#include "Widgets/TextureGoto.h"
#include "flowlayout/FlowLayout.h"
#include "toolwindowmanager/ToolWindowManagerArea.h"
#include "ui_TextureViewer.h"

float area(const QSizeF &s)
{
  return s.width() * s.height();
}

float aspect(const QSizeF &s)
{
  return s.width() / s.height();
}

static QMap<QString, ShaderEncoding> encodingExtensions = {
    {lit("hlsl"), ShaderEncoding::HLSL},
    {lit("glsl"), ShaderEncoding::GLSL},
    {lit("frag"), ShaderEncoding::GLSL},
    {lit("spvasm"), ShaderEncoding::SPIRVAsm},
};

Q_DECLARE_METATYPE(Following);

Following::Following(const TextureViewer &tex, FollowType t, ShaderStage s, int i, int a) : tex(tex)
{
  Type = t;
  Stage = s;
  index = i;
  arrayEl = a;
}

Following::Following(const Following &other) : tex(other.tex)
{
  Type = other.Type;
  Stage = other.Stage;
  index = other.index;
  arrayEl = other.arrayEl;
}

namespace FollowingInternal
{
TextureViewer *invalid = NULL;
};

Following::Following() : tex(*FollowingInternal::invalid)
{
  // we need a default constructor for QVariant but we don't want it to be valid, we always
  // initialise Following() with a TextureViewer reference.
  Type = FollowType::OutputColor;
  Stage = ShaderStage::Pixel;
  index = 0;
  arrayEl = 0;
}

Following &Following::operator=(const Following &other)
{
  Type = other.Type;
  Stage = other.Stage;
  index = other.index;
  arrayEl = other.arrayEl;
  return *this;
}

bool Following::operator!=(const Following &o)
{
  return !(*this == o);
}

bool Following::operator==(const Following &o)
{
  return Type == o.Type && Stage == o.Stage && index == o.index && arrayEl == o.arrayEl;
}

void Following::GetActionContext(ICaptureContext &ctx, bool &copy, bool &clear, bool &compute)
{
  const ActionDescription *curAction = ctx.CurAction();
  copy = curAction != NULL &&
         (curAction->flags & (ActionFlags::Copy | ActionFlags::Resolve | ActionFlags::Present));
  clear = curAction != NULL && (curAction->flags & ActionFlags::Clear);
  compute = curAction != NULL && (curAction->flags & ActionFlags::Dispatch) &&
            ctx.CurPipelineState().GetShader(ShaderStage::Compute) != ResourceId();
}

int Following::GetHighestMip(ICaptureContext &ctx)
{
  return GetBoundResource(ctx, arrayEl).firstMip;
}

int Following::GetFirstArraySlice(ICaptureContext &ctx)
{
  return GetBoundResource(ctx, arrayEl).firstSlice;
}

CompType Following::GetTypeHint(ICaptureContext &ctx)
{
  return GetBoundResource(ctx, arrayEl).typeCast;
}

ResourceId Following::GetResourceId(ICaptureContext &ctx)
{
  return GetBoundResource(ctx, arrayEl).resourceId;
}

BoundResource Following::GetBoundResource(ICaptureContext &ctx, int arrayIdx)
{
  BoundResource ret;

  if(Type == FollowType::OutputColor)
  {
    rdcarray<BoundResource> outputs = GetOutputTargets(ctx);

    if(index < outputs.count())
      ret = outputs[index];
  }
  else if(Type == FollowType::OutputDepth)
  {
    ret = GetDepthTarget(ctx);
  }
  else if(Type == FollowType::ReadWrite)
  {
    const rdcarray<BoundResourceArray> &rw = tex.m_ReadWriteResources[(int)Stage];

    ShaderBindpointMapping mapping = GetMapping(ctx);

    if(index < mapping.readWriteResources.count())
    {
      Bindpoint &key = mapping.readWriteResources[index];

      int residx = rw.indexOf(key);
      if(residx >= 0)
      {
        const BoundResourceArray &resArray = rw[residx];
        if(arrayIdx >= resArray.firstIndex &&
           arrayIdx - resArray.firstIndex < resArray.resources.count())
          ret = resArray.resources[arrayIdx - resArray.firstIndex];
      }
    }
  }
  else if(Type == FollowType::ReadOnly)
  {
    const rdcarray<BoundResourceArray> &ro = tex.m_ReadOnlyResources[(int)Stage];

    ShaderBindpointMapping mapping = GetMapping(ctx);

    if(index < mapping.readOnlyResources.count())
    {
      Bindpoint &key = mapping.readOnlyResources[index];

      int residx = ro.indexOf(key);
      if(residx >= 0)
      {
        const BoundResourceArray &resArray = ro[residx];
        if(arrayIdx >= resArray.firstIndex &&
           arrayIdx - resArray.firstIndex < resArray.resources.count())
          ret = resArray.resources[arrayIdx - resArray.firstIndex];
      }
    }
  }

  return ret;
}

rdcarray<BoundResource> Following::GetOutputTargets(ICaptureContext &ctx)
{
  const ActionDescription *curAction = ctx.CurAction();
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear)
  {
    return {BoundResource(curAction->copyDestination, curAction->copyDestinationSubresource)};
  }
  else if(compute)
  {
    return {};
  }
  else
  {
    rdcarray<BoundResource> ret = ctx.CurPipelineState().GetOutputTargets();

    if(ret.isEmpty() && curAction != NULL && (curAction->flags & ActionFlags::Present))
    {
      if(curAction->copyDestination != ResourceId())
        return {BoundResource(curAction->copyDestination, curAction->copyDestinationSubresource)};

      for(const TextureDescription &tex : ctx.GetTextures())
      {
        if(tex.creationFlags & TextureCategory::SwapBuffer)
          return {BoundResource(tex.resourceId)};
      }
    }

    return ret;
  }
}

BoundResource Following::GetDepthTarget(ICaptureContext &ctx)
{
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear || compute)
    return BoundResource(ResourceId());
  else
    return ctx.CurPipelineState().GetDepthTarget();
}

rdcarray<BoundResourceArray> Following::GetReadWriteResources(ICaptureContext &ctx,
                                                              ShaderStage stage, bool onlyUsed)
{
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear)
  {
    return rdcarray<BoundResourceArray>();
  }
  else if(compute)
  {
    // only return compute resources for one stage
    if(stage == ShaderStage::Pixel || stage == ShaderStage::Compute)
      return ctx.CurPipelineState().GetReadWriteResources(ShaderStage::Compute, onlyUsed);
    else
      return rdcarray<BoundResourceArray>();
  }
  else
  {
    return ctx.CurPipelineState().GetReadWriteResources(stage, onlyUsed);
  }
}

rdcarray<BoundResourceArray> Following::GetReadOnlyResources(ICaptureContext &ctx,
                                                             ShaderStage stage, bool onlyUsed)
{
  const ActionDescription *curAction = ctx.CurAction();
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear)
  {
    rdcarray<BoundResourceArray> ret;

    // only return copy source for one stage
    if(copy && stage == ShaderStage::Pixel)
      ret.push_back(BoundResourceArray(
          Bindpoint(0, 0), {BoundResource(curAction->copySource, curAction->copySourceSubresource)}));

    return ret;
  }
  else if(compute)
  {
    // only return compute resources for one stage
    if(stage == ShaderStage::Pixel || stage == ShaderStage::Compute)
      return ctx.CurPipelineState().GetReadOnlyResources(ShaderStage::Compute, onlyUsed);
    else
      return rdcarray<BoundResourceArray>();
  }
  else
  {
    return ctx.CurPipelineState().GetReadOnlyResources(stage, onlyUsed);
  }
}

const ShaderReflection *Following::GetReflection(ICaptureContext &ctx, ShaderStage stage)
{
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear)
    return NULL;
  else if(compute)
    return ctx.CurPipelineState().GetShaderReflection(ShaderStage::Compute);
  else
    return ctx.CurPipelineState().GetShaderReflection(stage);
}

const ShaderReflection *Following::GetReflection(ICaptureContext &ctx)
{
  return GetReflection(ctx, Stage);
}

const ShaderBindpointMapping &Following::GetMapping(ICaptureContext &ctx, ShaderStage stage)
{
  bool copy = false, clear = false, compute = false;
  GetActionContext(ctx, copy, clear, compute);

  if(copy || clear)
  {
    static ShaderBindpointMapping mapping;

    // for PS only add a single mapping to get the copy source
    if(copy && stage == ShaderStage::Pixel)
      mapping.readOnlyResources = {Bindpoint(0, 0)};
    else
      mapping.readOnlyResources.clear();

    return mapping;
  }
  else if(compute)
  {
    return ctx.CurPipelineState().GetBindpointMapping(ShaderStage::Compute);
  }
  else
  {
    return ctx.CurPipelineState().GetBindpointMapping(stage);
  }
}

const ShaderBindpointMapping &Following::GetMapping(ICaptureContext &ctx)
{
  return GetMapping(ctx, Stage);
}

namespace TextureListFilter
{
enum Columns
{
  Column_TexName,
  Column_TexWidth,
  Column_TexHeight,
  Column_TexDepth,
  Column_MipsCount,
  Column_TexFormat,
  Column_Count,
};
}

TextureDescription *TextureViewer::GetCurrentTexture()
{
  return m_CachedTexture;
}

void TextureViewer::UI_UpdateCachedTexture()
{
  if(!m_Ctx.IsCaptureLoaded())
  {
    m_CachedTexture = NULL;
    return;
  }

  ResourceId id = m_LockedId;
  if(id == ResourceId())
    id = m_Following.GetResourceId(m_Ctx);

  if(id == ResourceId())
    id = m_TexDisplay.resourceId;

  m_CachedTexture = m_Ctx.GetTexture(id);

  if(m_CachedTexture != NULL)
  {
    if(m_Ctx.APIProps().shaderDebugging)
    {
      const ShaderReflection *shaderDetails =
          m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Pixel);

      if(!m_Ctx.CurAction() || !(m_Ctx.CurAction()->flags & ActionFlags::Drawcall))
      {
        ui->debugPixelContext->setEnabled(false);
        ui->debugPixelContext->setToolTip(tr("No draw call selected"));
      }
      else if(!shaderDetails)
      {
        ui->debugPixelContext->setEnabled(false);
        ui->debugPixelContext->setToolTip(tr("No pixel shader bound"));
      }
      else if(!shaderDetails->debugInfo.debuggable)
      {
        ui->debugPixelContext->setEnabled(false);
        ui->debugPixelContext->setToolTip(
            tr("The current pixel shader does not support debugging: %1")
                .arg(shaderDetails->debugInfo.debugStatus));
      }
      else
      {
        ui->debugPixelContext->setEnabled(true);
        ui->debugPixelContext->setToolTip(QString());
      }
    }
    else
    {
      ui->debugPixelContext->setEnabled(false);
      ui->debugPixelContext->setToolTip(tr("Shader Debugging not supported on this API"));
    }

    if(m_Ctx.APIProps().pixelHistory)
    {
      ui->pixelHistory->setEnabled(true);
      ui->pixelHistory->setToolTip(QString());
    }
    else
    {
      ui->pixelHistory->setEnabled(false);
      ui->pixelHistory->setToolTip(tr("Pixel History not supported on this API"));
    }
  }
  else
  {
    ui->debugPixelContext->setEnabled(false);
    ui->debugPixelContext->setToolTip(tr("No active texture selected"));
    ui->pixelHistory->setEnabled(false);
    ui->pixelHistory->setToolTip(tr("No active texture selected"));
  }
}

TextureViewer::TextureViewer(ICaptureContext &ctx, QWidget *parent)
    : QFrame(parent),
      ui(new Ui::TextureViewer),
      m_Ctx(ctx),
      m_Following(*this, FollowType::OutputColor, ShaderStage::Pixel, 0, 0)
{
  ui->setupUi(this);

  ui->render->SetContext(m_Ctx);
  ui->pixelContext->SetContext(m_Ctx);

  ui->textureList->setFont(Formatter::PreferredFont());
  ui->textureListFilter->setFont(Formatter::PreferredFont());
  ui->rangeBlack->setFont(Formatter::PreferredFont());
  ui->rangeWhite->setFont(Formatter::PreferredFont());
  ui->hdrMul->setFont(Formatter::PreferredFont());
  ui->channels->setFont(Formatter::PreferredFont());
  ui->mipLevel->setFont(Formatter::PreferredFont());
  ui->sliceFace->setFont(Formatter::PreferredFont());
  ui->zoomOption->setFont(Formatter::PreferredFont());

  Reset();

  on_checkerBack_clicked();

  QObject::connect(ui->zoomOption->lineEdit(), &QLineEdit::returnPressed, this,
                   &TextureViewer::zoomOption_returnPressed);

  QObject::connect(ui->depthDisplay, &QToolButton::toggled, this,
                   &TextureViewer::channelsWidget_toggled);
  QObject::connect(ui->stencilDisplay, &QToolButton::toggled, this,
                   &TextureViewer::channelsWidget_toggled);
  QObject::connect(ui->flip_y, &QToolButton::toggled, this, &TextureViewer::channelsWidget_toggled);
  QObject::connect(ui->gammaDisplay, &QToolButton::toggled, this,
                   &TextureViewer::channelsWidget_toggled);
  QObject::connect(ui->channels, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged), this,
                   &TextureViewer::channelsWidget_selected);
  QObject::connect(ui->hdrMul, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged), this,
                   &TextureViewer::channelsWidget_selected);
  QObject::connect(ui->hdrMul, &QComboBox::currentTextChanged, [this] { UI_UpdateChannels(); });
  QObject::connect(ui->customShader, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged), this,
                   &TextureViewer::channelsWidget_selected);
  QObject::connect(ui->customShader, &QComboBox::currentTextChanged, [this] { UI_UpdateChannels(); });
  QObject::connect(ui->rangeHistogram, &RangeHistogram::rangeUpdated, this,
                   &TextureViewer::range_rangeUpdated);
  QObject::connect(ui->rangeBlack, &RDLineEdit::textChanged, this,
                   &TextureViewer::rangePoint_textChanged);
  QObject::connect(ui->rangeBlack, &RDLineEdit::leave, this, &TextureViewer::rangePoint_leave);
  QObject::connect(ui->rangeBlack, &RDLineEdit::keyPress, this, &TextureViewer::rangePoint_keyPress);
  QObject::connect(ui->rangeWhite, &RDLineEdit::textChanged, this,
                   &TextureViewer::rangePoint_textChanged);
  QObject::connect(ui->rangeWhite, &RDLineEdit::leave, this, &TextureViewer::rangePoint_leave);
  QObject::connect(ui->rangeWhite, &RDLineEdit::keyPress, this, &TextureViewer::rangePoint_keyPress);

  for(RDToolButton *butt : {ui->channelRed, ui->channelGreen, ui->channelBlue, ui->channelAlpha})
  {
    QObject::connect(butt, &RDToolButton::toggled, this, &TextureViewer::channelsWidget_toggled);
    QObject::connect(butt, &RDToolButton::mouseClicked, this,
                     &TextureViewer::channelsWidget_mouseClicked);
    QObject::connect(butt, &RDToolButton::doubleClicked, this,
                     &TextureViewer::channelsWidget_mouseClicked);
  }

  {
    QMenu *extensionsMenu = new QMenu(this);

    ui->extensions->setMenu(extensionsMenu);
    ui->extensions->setPopupMode(QToolButton::InstantPopup);

    QObject::connect(extensionsMenu, &QMenu::aboutToShow, [this, extensionsMenu]() {
      extensionsMenu->clear();
      m_Ctx.Extensions().MenuDisplaying(PanelMenu::TextureViewer, extensionsMenu, ui->extensions, {});
    });
  }

  QObject::connect(ui->textureList, &RDTreeWidget::itemActivated, this,
                   &TextureViewer::texture_itemActivated);

  QWidget *renderContainer = ui->renderContainer;

  ui->dockarea->addToolWindow(ui->renderContainer, ToolWindowManager::EmptySpace);
  ui->dockarea->setToolWindowProperties(
      renderContainer, ToolWindowManager::DisallowUserDocking | ToolWindowManager::HideCloseButton |
                           ToolWindowManager::DisableDraggableTab |
                           ToolWindowManager::AlwaysDisplayFullTabs);

  ui->dockarea->addToolWindow(
      ui->textureListFrame,
      ToolWindowManager::AreaReference(ToolWindowManager::NoArea,
                                       ui->dockarea->areaOf(renderContainer), 0.25f));
  ui->dockarea->setToolWindowProperties(ui->textureListFrame, ToolWindowManager::HideOnClose);

  ui->dockarea->addToolWindow(ui->inputThumbs, ToolWindowManager::AreaReference(
                                                   ToolWindowManager::RightOf,
                                                   ui->dockarea->areaOf(renderContainer), 0.25f));
  ui->dockarea->setToolWindowProperties(ui->inputThumbs, ToolWindowManager::HideCloseButton);

  ui->dockarea->addToolWindow(
      ui->outputThumbs, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
                                                         ui->dockarea->areaOf(ui->inputThumbs)));
  ui->dockarea->setToolWindowProperties(ui->outputThumbs, ToolWindowManager::HideCloseButton);

  ui->dockarea->addToolWindow(
      ui->pixelContextLayout,
      ToolWindowManager::AreaReference(ToolWindowManager::BottomOf,
                                       ui->dockarea->areaOf(ui->outputThumbs), 0.25f));
  ui->dockarea->setToolWindowProperties(ui->pixelContextLayout, ToolWindowManager::HideCloseButton);

  ui->dockarea->setAllowFloatingWindow(false);

  renderContainer->setWindowTitle(tr("Unbound"));
  ui->pixelContextLayout->setWindowTitle(tr("Pixel Context"));
  ui->outputThumbs->setWindowTitle(tr("Outputs"));
  ui->inputThumbs->setWindowTitle(tr("Inputs"));
  ui->textureListFrame->setWindowTitle(tr("Texture List"));

  m_Goto = new TextureGoto(this, [this](QPoint p) { GotoLocation(p.x(), p.y()); });

  QVBoxLayout *vertical = new QVBoxLayout(this);

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

  QWidget *flow1widget = new QWidget(this);
  QWidget *flow2widget = new QWidget(this);

  FlowLayout *flow1 = new FlowLayout(flow1widget, 0, 3, 3);
  FlowLayout *flow2 = new FlowLayout(flow2widget, 0, 3, 3);

  flow1widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
  flow2widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);

  flow1->addWidget(ui->channelsToolbar);
  flow1->addWidget(ui->subresourceToolbar);
  flow1->addWidget(ui->actionToolbar);

  flow2->addWidget(ui->zoomToolbar);
  flow2->addWidget(ui->overlayToolbar);
  flow2->addWidget(ui->rangeToolbar);

  vertical->addWidget(flow1widget);
  vertical->addWidget(flow2widget);
  vertical->addWidget(ui->dockarea);

  Ui_TextureViewer *u = ui;
  u->pixelcontextgrid->setAlignment(u->pixelHistory, Qt::AlignCenter);
  u->pixelcontextgrid->setAlignment(u->debugPixelContext, Qt::AlignCenter);

  QWidget *statusflowWidget = new QWidget(this);

  ui->statusbar->removeWidget(ui->texStatusName);
  ui->statusbar->removeWidget(ui->texStatusDim);
  ui->statusbar->removeWidget(ui->texStatusFormat);
  ui->statusbar->removeWidget(ui->pickSwatch);
  ui->statusbar->removeWidget(ui->hoverText);
  ui->statusbar->removeWidget(ui->pickedText);

  FlowLayout *statusflow = new FlowLayout(0, 3, 0);

  statusflowWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);

  statusflow->addWidget(ui->texStatusName);
  statusflow->addWidget(ui->texStatusDim);
  statusflow->addWidget(ui->texStatusFormat);
  statusflow->addWidget(ui->pickSwatch);
  statusflow->addWidget(ui->hoverText);
  statusflow->addWidget(ui->pickedText);

  ui->texStatusName->setFont(Formatter::FixedFont());
  ui->texStatusDim->setFont(Formatter::FixedFont());
  ui->texStatusFormat->setFont(Formatter::FixedFont());
  ui->hoverText->setFont(Formatter::FixedFont());
  ui->pickedText->setFont(Formatter::FixedFont());

  ui->renderLayout->removeItem(ui->statusbar);
  ui->renderLayout->addItem(statusflow);

  ui->channels->addItems({lit("RGBA"), lit("RGBM"), lit("YUVA decode"), tr("Custom")});

  ui->zoomOption->addItems({lit("10%"), lit("25%"), lit("50%"), lit("75%"), lit("100%"),
                            lit("200%"), lit("400%"), lit("800%")});

  ui->hdrMul->addItems({lit("2"), lit("4"), lit("8"), lit("16"), lit("32"), lit("128")});

  ui->overlay->addItems({tr("None"), tr("Highlight Drawcall"), tr("Wireframe Mesh"),
                         tr("Depth Test"), tr("Stencil Test"), tr("Backface Cull"),
                         tr("Viewport/Scissor Region"), tr("NaN/INF/-ve Display"),
                         tr("Histogram Clipping"), tr("Clear Before Pass"), tr("Clear Before Draw"),
                         tr("Quad Overdraw (Pass)"), tr("Quad Overdraw (Draw)"),
                         tr("Triangle Size (Pass)"), tr("Triangle Size (Draw)")});

  ui->textureListFilter->addItems({QString(), tr("Textures"), tr("Render Targets")});

  ui->textureList->setColumns({tr("Texture Name"), tr("Width"), tr("Height"), tr("Depth/Slices"),
                               tr("Mips Count"), tr("Format"), tr("Go")});
  ui->textureList->setHoverIconColumn(6, Icons::action(), Icons::action_hover());
  ui->textureList->viewport()->setAttribute(Qt::WA_Hover);
  ui->textureList->setMouseTracking(true);
  {
    RDHeaderView *header = new RDHeaderView(Qt::Horizontal, this);
    ui->textureList->setHeader(header);

    header->setColumnStretchHints({1, -1, -1, -1, -1, -1, -1});
  }

  ui->textureList->sortByColumn(TextureListFilter::Column_TexName, Qt::SortOrder::AscendingOrder);

  ui->zoomOption->setCurrentText(QString());
  ui->fitToWindow->toggle();

  m_Ctx.AddCaptureViewer(this);

  SetupTextureTabs();

  QObject::connect(ui->render, &CustomPaintWidget::clicked, this, &TextureViewer::render_mouseClick);
  QObject::connect(ui->render, &CustomPaintWidget::mouseMove, this, &TextureViewer::render_mouseMove);
  QObject::connect(ui->render, &CustomPaintWidget::mouseWheel, this,
                   &TextureViewer::render_mouseWheel);
  QObject::connect(ui->render, &CustomPaintWidget::resize, this, &TextureViewer::render_resize);
  QObject::connect(ui->render, &CustomPaintWidget::keyPress, this, &TextureViewer::render_keyPress);

  QObject::connect(ui->pixelContext, &CustomPaintWidget::keyPress, this,
                   &TextureViewer::render_keyPress);
}

TextureViewer::~TextureViewer()
{
  if(m_Output)
  {
    m_Ctx.Replay().BlockInvoke([this](IReplayController *r) { m_Output->Shutdown(); });
  }

  m_Ctx.BuiltinWindowClosed(this);
  m_Ctx.RemoveCaptureViewer(this);
  delete ui;
}

void TextureViewer::enterEvent(QEvent *event)
{
  HighlightUsage();
}

void TextureViewer::showEvent(QShowEvent *event)
{
  HighlightUsage();
}

void TextureViewer::HighlightUsage()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr && m_Ctx.HasTimelineBar())
    m_Ctx.GetTimelineBar()->HighlightResourceUsage(texptr->resourceId);
}

void TextureViewer::SelectPreview(ResourcePreview *prev)
{
  Following follow = prev->property("f").value<Following>();

  for(ResourcePreview *p : ui->outputThumbs->thumbs())
    p->setSelected(false);

  for(ResourcePreview *p : ui->inputThumbs->thumbs())
    p->setSelected(false);

  m_Following = Following(follow);
  prev->setSelected(true);

  UI_UpdateCachedTexture();

  ResourceId id = m_Following.GetResourceId(m_Ctx);

  if(id != ResourceId())
  {
    UI_OnTextureSelectionChanged(false);
    ui->renderContainer->show();
  }
}

void TextureViewer::RT_FetchCurrentPixel(IReplayController *r, uint32_t x, uint32_t y,
                                         PixelValue &pickValue, PixelValue &realValue)
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr == NULL)
    return;

  if(m_TexDisplay.flipY)
    y = (texptr->height - 1) - y;

  x = qMax(0U, x >> m_TexDisplay.subresource.mip);
  y = qMax(0U, y >> m_TexDisplay.subresource.mip);

  ResourceId id = m_TexDisplay.resourceId;
  Subresource sub = m_TexDisplay.subresource;
  CompType typeCast = m_TexDisplay.typeCast;

  if(m_TexDisplay.overlay == DebugOverlay::QuadOverdrawDraw ||
     m_TexDisplay.overlay == DebugOverlay::QuadOverdrawPass ||
     m_TexDisplay.overlay == DebugOverlay::TriangleSizeDraw ||
     m_TexDisplay.overlay == DebugOverlay::TriangleSizePass)
  {
    ResourceId overlayResId = m_Output->GetDebugOverlayTexID();

    if(overlayResId != ResourceId())
    {
      id = overlayResId;
      typeCast = CompType::Typeless;
    }
  }

  realValue = r->PickPixel(id, x, y, sub, typeCast);

  if(m_TexDisplay.customShaderId != ResourceId())
  {
    pickValue = r->PickPixel(m_Output->GetCustomShaderTexID(), x, y,
                             {m_TexDisplay.subresource.mip, 0, 0}, CompType::Typeless);
  }
  else
  {
    pickValue = realValue;
  }
}

void TextureViewer::RT_PickPixelsAndUpdate(IReplayController *r)
{
  PixelValue pickValue, realValue;

  if(m_PickedPoint.x() < 0 || m_PickedPoint.y() < 0)
    return;

  uint32_t x = (uint32_t)m_PickedPoint.x();
  uint32_t y = (uint32_t)m_PickedPoint.y();

  RT_FetchCurrentPixel(r, x, y, pickValue, realValue);

  m_Output->SetPixelContextLocation(x, y);

  m_CurHoverValue = pickValue;

  m_CurPixelValue = pickValue;
  m_CurRealValue = realValue;

  GUIInvoke::call(this, [this]() { UI_UpdateStatusText(); });
}

void TextureViewer::RT_PickHoverAndUpdate(IReplayController *r)
{
  PixelValue pickValue, realValue;

  uint32_t x = (uint32_t)m_CurHoverPixel.x();
  uint32_t y = (uint32_t)m_CurHoverPixel.y();

  RT_FetchCurrentPixel(r, x, y, pickValue, realValue);

  m_CurHoverValue = pickValue;

  GUIInvoke::call(this, [this]() { UI_UpdateStatusText(); });
}

void TextureViewer::RT_UpdateAndDisplay(IReplayController *r)
{
  if(m_Output != NULL)
    m_Output->SetTextureDisplay(m_TexDisplay);

  GUIInvoke::call(this, [this]() { ui->render->update(); });
}

void TextureViewer::RT_UpdateVisualRange(IReplayController *r)
{
  TextureDescription *texptr = GetCurrentTexture();

  if(!m_Visualise || texptr == NULL || m_Output == NULL)
    return;

  TextureDescription &tex = *texptr;

  ResourceFormat fmt = tex.format;

  bool uintTex = (tex.format.compType == CompType::UInt);
  bool sintTex = (tex.format.compType == CompType::SInt);

  if(tex.format.compType == CompType::Typeless && m_TexDisplay.typeCast == CompType::UInt)
    uintTex = true;

  if(tex.format.compType == CompType::Typeless && m_TexDisplay.typeCast == CompType::SInt)
    sintTex = true;

  if(m_TexDisplay.customShaderId != ResourceId())
    fmt.compCount = 4;

  rdcfixedarray<bool, 4> channels = {
      m_TexDisplay.red ? true : false, m_TexDisplay.green && fmt.compCount > 1,
      m_TexDisplay.blue && fmt.compCount > 2, m_TexDisplay.alpha && fmt.compCount > 3,
  };

  ResourceId textureId = m_TexDisplay.resourceId;
  Subresource sub = m_TexDisplay.subresource;
  CompType typeCast = m_TexDisplay.typeCast;

  if(m_TexDisplay.customShaderId != ResourceId() && m_Output->GetCustomShaderTexID() != ResourceId())
  {
    textureId = m_Output->GetCustomShaderTexID();
    sub.slice = sub.sample = 0;
    typeCast = CompType::Typeless;
  }

  PixelValue min, max;
  rdctie(min, max) = r->GetMinMax(textureId, sub, typeCast);

  // exclude any channels where the min == max, as this destroys the histogram's utility.
  // When we do this, after we have the histogram we set the appropriate bucket to max - to still
  // show that there was data there but it's "clamped".
  float excludedBucket[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
  const float rangeSize = ui->rangeHistogram->rangeMax() - ui->rangeHistogram->rangeMin();
  for(int i = 0; i < 4; i++)
  {
    if(min.uintValue[i] == max.uintValue[i])
    {
      channels[i] = false;

      if(uintTex)
        excludedBucket[i] = (float(min.uintValue[i]) - ui->rangeHistogram->rangeMin()) / rangeSize;
      else if(sintTex)
        excludedBucket[i] = (float(min.intValue[i]) - ui->rangeHistogram->rangeMin()) / rangeSize;
      else
        excludedBucket[i] = (min.floatValue[i] - ui->rangeHistogram->rangeMin()) / rangeSize;
    }
  }

  rdcarray<uint32_t> histogram =
      r->GetHistogram(textureId, sub, typeCast, ui->rangeHistogram->rangeMin(),
                      ui->rangeHistogram->rangeMax(), channels);

  if(!histogram.empty())
  {
    QVector<uint32_t> histogramVec(histogram.count());
    if(!histogram.isEmpty())
      memcpy(histogramVec.data(), histogram.data(), histogram.byteSize());

    // if the histogram is completely empty we still want to set 1 value in there.
    uint32_t maxval = 1;
    for(const uint32_t &v : histogramVec)
      maxval = qMax(v, maxval);

    if(!histogramVec.isEmpty())
    {
      for(int i = 0; i < 4; i++)
      {
        int bucket = excludedBucket[i] * (histogramVec.size() - 1);
        if(bucket >= 0 && bucket < histogramVec.size())
          histogramVec[bucket] = maxval;
      }
    }

    GUIInvoke::call(this, [this, histogramVec]() {
      ui->rangeHistogram->setHistogramRange(ui->rangeHistogram->rangeMin(),
                                            ui->rangeHistogram->rangeMax());
      ui->rangeHistogram->setHistogramData(histogramVec);
    });
  }
}

void TextureViewer::UI_UpdateStatusText()
{
  TextureDescription *texptr = GetCurrentTexture();
  if(texptr == NULL)
    return;

  TextureDescription &tex = *texptr;

  CompType compType = tex.format.compType;

  const bool yuv =
      (tex.format.type == ResourceFormatType::YUV8 || tex.format.type == ResourceFormatType::YUV10 ||
       tex.format.type == ResourceFormatType::YUV12 || tex.format.type == ResourceFormatType::YUV16);

  if(tex.format.compType != m_TexDisplay.typeCast && m_TexDisplay.typeCast != CompType::Typeless &&
     !yuv)
    compType = m_TexDisplay.typeCast;

  bool dsv = (tex.creationFlags & TextureCategory::DepthTarget) || (compType == CompType::Depth);
  bool uintTex = (compType == CompType::UInt);
  bool sintTex = (compType == CompType::SInt);

  if(m_TexDisplay.overlay == DebugOverlay::QuadOverdrawPass ||
     m_TexDisplay.overlay == DebugOverlay::QuadOverdrawDraw ||
     m_TexDisplay.overlay == DebugOverlay::TriangleSizePass ||
     m_TexDisplay.overlay == DebugOverlay::TriangleSizeDraw)
  {
    dsv = false;
    uintTex = false;
    sintTex = false;
  }

  QColor swatchColor;

  if(dsv || uintTex || sintTex)
  {
    swatchColor = QColor(0, 0, 0);
  }
  else
  {
    float r = qBound(0.0f, m_CurHoverValue.floatValue[0], 1.0f);
    float g = qBound(0.0f, m_CurHoverValue.floatValue[1], 1.0f);
    float b = qBound(0.0f, m_CurHoverValue.floatValue[2], 1.0f);

    if(tex.format.SRGBCorrected() || (tex.creationFlags & TextureCategory::SwapBuffer))
    {
      r = powf(r, 1.0f / 2.2f);
      g = powf(g, 1.0f / 2.2f);
      b = powf(b, 1.0f / 2.2f);
    }

    swatchColor = QColor(int(255.0f * r), int(255.0f * g), int(255.0f * b));
  }

  {
    QPalette Pal(palette());

    Pal.setColor(QPalette::Background, swatchColor);

    ui->pickSwatch->setAutoFillBackground(true);
    ui->pickSwatch->setPalette(Pal);
  }

  int y = m_CurHoverPixel.y() >> (int)m_TexDisplay.subresource.mip;

  uint32_t mipWidth = qMax(1U, tex.width >> (int)m_TexDisplay.subresource.mip);
  uint32_t mipHeight = qMax(1U, tex.height >> (int)m_TexDisplay.subresource.mip);

  if(ShouldFlipForGL())
    y = (int)(mipHeight - 1) - y;
  if(m_TexDisplay.flipY)
    y = (int)(mipHeight - 1) - y;

  y = qMax(0, y);

  int x = m_CurHoverPixel.x() >> (int)m_TexDisplay.subresource.mip;
  float invWidth = 1.0f / mipWidth;
  float invHeight = 1.0f / mipHeight;

  QString hoverCoords = QFormatStr("%1, %2 (%3, %4)")
                            .arg(x, 4)
                            .arg(y, 4)
                            .arg((x * invWidth), 5, 'f', 4)
                            .arg((y * invHeight), 5, 'f', 4);

  QString hoverText;

  uint32_t hoverX = (uint32_t)m_CurHoverPixel.x();
  uint32_t hoverY = (uint32_t)m_CurHoverPixel.y();

  if(hoverX > tex.width || hoverY > tex.height)
    hoverText = tr("Hover - [%1] - ").arg(hoverCoords);
  else
    hoverText = tr("Hover -  %1  - ").arg(hoverCoords);

  ui->hoverText->setText(hoverText);

  QString pickedText;
  QString pickedTooltip;

  if(m_PickedPoint.x() >= 0)
  {
    x = m_PickedPoint.x() >> (int)m_TexDisplay.subresource.mip;
    y = m_PickedPoint.y() >> (int)m_TexDisplay.subresource.mip;
    if(ShouldFlipForGL())
      y = (int)(mipHeight - 1) - y;
    if(m_TexDisplay.flipY)
      y = (int)(mipHeight - 1) - y;

    y = qMax(0, y);

    pickedText = tr("Right click - %1, %2: ").arg(x, 4).arg(y, 4);

    PixelValue val = m_CurPixelValue;

    if(m_TexDisplay.customShaderId != ResourceId())
    {
      pickedText += QFormatStr("%1, %2, %3, %4")
                        .arg(Formatter::Format(val.floatValue[0]))
                        .arg(Formatter::Format(val.floatValue[1]))
                        .arg(Formatter::Format(val.floatValue[2]))
                        .arg(Formatter::Format(val.floatValue[3]));

      val = m_CurRealValue;

      pickedText += tr(" (Real: ");
    }

    if(tex.format.type == ResourceFormatType::A8)
      val.floatValue[0] = val.floatValue[3];

    if(dsv)
    {
      pickedText += tr("Depth ");
      if(uintTex)
      {
        pickedText += Formatter::Format(val.uintValue[0]);
      }
      else
      {
        pickedText += Formatter::Format(val.floatValue[0]);
      }

      if(tex.format.type == ResourceFormatType::D16S8 ||
         tex.format.type == ResourceFormatType::D24S8 ||
         tex.format.type == ResourceFormatType::D32S8 || tex.format.type == ResourceFormatType::S8)
      {
        int stencil = (int)(255.0f * val.floatValue[1]);

        if(tex.format.type == ResourceFormatType::S8)
          pickedText.clear();
        else
          pickedText += lit(", ");

        pickedText += tr("Stencil 0x%1").arg(Formatter::Format(uint8_t(stencil & 0xff), true));

        pickedTooltip = tr("Stencil: %1 / 0x%2 / 0b%3")
                            .arg(stencil, 3, 10, QLatin1Char(' '))
                            .arg(Formatter::Format(uint8_t(stencil & 0xff), true))
                            .arg(stencil, 8, 2, QLatin1Char('0'));
      }
    }
    else
    {
      // Restrict the number of components displayed to the component count of the resource format
      if(uintTex)
      {
        pickedText += QFormatStr("%1").arg(Formatter::Format(val.uintValue[0]));
        if(tex.format.compCount > 1)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.uintValue[1]));
        if(tex.format.compCount > 2)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.uintValue[2]));
        if(tex.format.compCount > 3)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.uintValue[3]));
      }
      else if(sintTex)
      {
        pickedText += QFormatStr("%1").arg(Formatter::Format(val.intValue[0]));
        if(tex.format.compCount > 1)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.intValue[1]));
        if(tex.format.compCount > 2)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.intValue[2]));
        if(tex.format.compCount > 3)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.intValue[3]));
      }
      else
      {
        pickedText += QFormatStr("%1").arg(Formatter::Format(val.floatValue[0]));
        if(tex.format.compCount > 1)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.floatValue[1]));
        if(tex.format.compCount > 2)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.floatValue[2]));
        if(tex.format.compCount > 3)
          pickedText += QFormatStr(", %1").arg(Formatter::Format(val.floatValue[3]));
      }
    }

    if(m_TexDisplay.customShaderId != ResourceId())
      pickedText += lit(")");
  }
  else
  {
    pickedText += tr("Right click to pick a pixel");
  }

  // try and keep status text consistent by sticking to the high water mark
  // of length (prevents nasty oscillation when the length of the string is
  // just popping over/under enough to overflow onto the next line).

  if(pickedText.length() > m_HighWaterStatusLength)
    m_HighWaterStatusLength = pickedText.length();

  if(pickedText.length() < m_HighWaterStatusLength)
    pickedText += QString(m_HighWaterStatusLength - pickedText.length(), QLatin1Char(' '));

  ui->pickedText->setText(pickedText);
  ui->pickedText->setToolTip(pickedTooltip);
}

void TextureViewer::UI_UpdateTextureDetails()
{
  QString status;

  TextureDescription *texptr = GetCurrentTexture();
  if(texptr == NULL)
  {
    ui->texStatusName->setText(status);
    ui->texStatusDim->setText(status);
    ui->texStatusFormat->setText(status);

    ui->renderContainer->setWindowTitle(tr("Unbound"));
    return;
  }

  TextureDescription &current = *texptr;

  ResourceId followID = m_Following.GetResourceId(m_Ctx);

  {
    TextureDescription *followtex = m_Ctx.GetTexture(followID);
    BufferDescription *followbuf = m_Ctx.GetBuffer(followID);

    QString title;

    if(followID == ResourceId())
    {
      title = tr("Unbound");
    }
    else if(followtex || followbuf)
    {
      QString name = m_Ctx.GetResourceName(followID);

      switch(m_Following.Type)
      {
        case FollowType::OutputColor:
          title = QString(tr("Cur Output %1 - %2")).arg(m_Following.index).arg(name);
          break;
        case FollowType::OutputDepth: title = QString(tr("Cur Depth Output - %1")).arg(name); break;
        case FollowType::ReadWrite:
          title = QString(tr("Cur RW Output %1 - %2")).arg(m_Following.index).arg(name);
          break;
        case FollowType::ReadOnly:
          title = QString(tr("Cur Input %1 - %2")).arg(m_Following.index).arg(name);
          break;
      }
    }
    else
    {
      switch(m_Following.Type)
      {
        case FollowType::OutputColor:
          title = QString(tr("Cur Output %1")).arg(m_Following.index);
          break;
        case FollowType::OutputDepth: title = QString(tr("Cur Depth Output")); break;
        case FollowType::ReadWrite:
          title = QString(tr("Cur RW Output %1")).arg(m_Following.index);
          break;
        case FollowType::ReadOnly:
          title = QString(tr("Cur Input %1")).arg(m_Following.index);
          break;
      }
    }

    ui->renderContainer->setWindowTitle(title);
  }

  ui->texStatusName->setText(m_Ctx.GetResourceName(current.resourceId) + lit(" - "));

  status = QString();

  if(current.dimension >= 1)
    status += QString::number(current.width);
  if(current.dimension >= 2)
    status += lit("x") + QString::number(current.height);
  if(current.dimension >= 3)
    status += lit("x") + QString::number(current.depth);

  if(current.arraysize > 1)
    status += QFormatStr("[%1]").arg(QString::number(current.arraysize));

  if(current.msQual > 0 || current.msSamp > 1)
    status += QFormatStr(" MS{%1x %2Q}").arg(current.msSamp).arg(current.msQual);

  status += QFormatStr(" %1 mips").arg(current.mips);

  status += lit(" - ");

  ui->texStatusDim->setText(status);

  status = current.format.Name();

  const bool yuv = (current.format.type == ResourceFormatType::YUV8 ||
                    current.format.type == ResourceFormatType::YUV10 ||
                    current.format.type == ResourceFormatType::YUV12 ||
                    current.format.type == ResourceFormatType::YUV16);

  CompType viewCast = CompType::Typeless;

  if(current.format.compType != m_TexDisplay.typeCast &&
     m_TexDisplay.typeCast != CompType::Typeless && !yuv)
  {
    viewCast = m_TexDisplay.typeCast;
  }
  else if(current.format.compType == CompType::Typeless &&
          m_TexDisplay.typeCast == CompType::Typeless && !yuv)
  {
    // if it's a typeless texture and we don't have a hint, ensure the user knows it's being viewed
    // as unorm as a fallback
    viewCast = CompType::UNorm;
  }

  if(viewCast != CompType::Typeless)
    status += tr(" Viewed as %1").arg(ToQStr(viewCast));

  ui->texStatusFormat->setText(status);
}

void TextureViewer::UI_OnTextureSelectionChanged(bool newAction)
{
  TextureDescription *texptr = GetCurrentTexture();

  // reset high-water mark
  m_HighWaterStatusLength = 0;

  if(texptr == NULL)
    return;

  TextureDescription &tex = *texptr;

  bool newtex = (m_TexDisplay.resourceId != tex.resourceId);

  // save settings for this current texture
  if(m_Ctx.Config().TextureViewer_PerTexSettings)
  {
    m_TextureSettings[m_TexDisplay.resourceId].r = ui->channelRed->isChecked();
    m_TextureSettings[m_TexDisplay.resourceId].g = ui->channelGreen->isChecked();
    m_TextureSettings[m_TexDisplay.resourceId].b = ui->channelBlue->isChecked();
    m_TextureSettings[m_TexDisplay.resourceId].a = ui->channelAlpha->isChecked();

    // save state regardless, we just don't apply it without the setting
    m_TextureSettings[m_TexDisplay.resourceId].flip_y = ui->flip_y->isChecked();

    m_TextureSettings[m_TexDisplay.resourceId].displayType = qMax(0, ui->channels->currentIndex());
    m_TextureSettings[m_TexDisplay.resourceId].customShader = ui->customShader->currentText();

    m_TextureSettings[m_TexDisplay.resourceId].depth = ui->depthDisplay->isChecked();
    m_TextureSettings[m_TexDisplay.resourceId].stencil = ui->stencilDisplay->isChecked();

    m_TextureSettings[m_TexDisplay.resourceId].mip = qMax(0, ui->mipLevel->currentIndex());
    m_TextureSettings[m_TexDisplay.resourceId].slice = qMax(0, ui->sliceFace->currentIndex());

    m_TextureSettings[m_TexDisplay.resourceId].minrange = ui->rangeHistogram->blackPoint();
    m_TextureSettings[m_TexDisplay.resourceId].maxrange = ui->rangeHistogram->whitePoint();

    if(m_TexDisplay.typeCast != CompType::Typeless)
      m_TextureSettings[m_TexDisplay.resourceId].typeCast = m_TexDisplay.typeCast;
  }

  m_TexDisplay.resourceId = tex.resourceId;

  // interpret the texture according to the currently following type.
  if(!currentTextureIsLocked())
    m_TexDisplay.typeCast = m_Following.GetTypeHint(m_Ctx);
  else
    m_TexDisplay.typeCast = CompType::Typeless;

  // if there is no such type or it isn't being followed, use the last seen interpretation
  if(m_TexDisplay.typeCast == CompType::Typeless &&
     m_TextureSettings.contains(m_TexDisplay.resourceId))
    m_TexDisplay.typeCast = m_TextureSettings[m_TexDisplay.resourceId].typeCast;

  // try to maintain the pan in the new texture. If the new texture
  // is approx an integer multiple of the old texture, just changing
  // the scale will keep everything the same. This is useful for
  // downsample chains and things where you're flipping back and forth
  // between overlapping textures, but even in the non-integer case
  // pan will be kept approximately the same.
  QSizeF curSize((float)tex.width, (float)tex.height);
  float curArea = area(curSize);
  float prevArea = area(m_PrevSize);

  if(prevArea > 0.0f && m_PrevSize.width() > 0.0f)
  {
    float prevX = m_TexDisplay.xOffset;
    float prevY = m_TexDisplay.yOffset;

    // allow slight difference in aspect ratio for rounding errors
    // in downscales (e.g. 1680x1050 -> 840x525 -> 420x262 in the
    // last downscale the ratios are 1.6 and 1.603053435).
    if(qAbs(aspect(curSize) - aspect(m_PrevSize)) < 0.01f)
    {
      m_TexDisplay.scale *= m_PrevSize.width() / curSize.width();
      setCurrentZoomValue(m_TexDisplay.scale);
    }
    else
    {
      // this scale factor is arbitrary really, only intention is to have
      // integer scales come out precisely, other 'similar' sizes will be
      // similar ish
      float scaleFactor = (float)(sqrt(curArea) / sqrt(prevArea));

      m_TexDisplay.xOffset = prevX * scaleFactor;
      m_TexDisplay.yOffset = prevY * scaleFactor;
    }
  }

  m_PrevSize = curSize;

  // refresh scroll position
  UI_CalcScrollbars();
  setScrollPosition(getScrollPosition());

  UI_UpdateStatusText();

  // block signals for mipLevel and sliceFace comboboxes while editing them
  ui->mipLevel->blockSignals(true);
  ui->sliceFace->blockSignals(true);

  ui->mipLevel->clear();

  m_TexDisplay.subresource.mip = 0;
  m_TexDisplay.subresource.slice = 0;

  bool usemipsettings = true;
  bool useslicesettings = true;

  if(tex.msSamp > 1)
  {
    for(uint32_t i = 0; i < tex.msSamp; i++)
      ui->mipLevel->addItem(tr("Sample %1").arg(i));

    // add an option to display unweighted average resolved value,
    // to get an idea of how the samples average
    if(tex.format.compType != CompType::UInt && tex.format.compType != CompType::SInt &&
       tex.format.compType != CompType::Depth && !(tex.creationFlags & TextureCategory::DepthTarget))
      ui->mipLevel->addItem(tr("Average val"));

    ui->mipLabel->setText(tr("Sample"));

    ui->mipLevel->setCurrentIndex(0);
  }
  else
  {
    for(uint32_t i = 0; i < tex.mips; i++)
      ui->mipLevel->addItem(
          QFormatStr("%1 - %2x%3").arg(i).arg(qMax(1U, tex.width >> i)).arg(qMax(1U, tex.height >> i)));

    ui->mipLabel->setText(tr("Mip"));
  }

  if(tex.mips == 1 && tex.msSamp <= 1)
    ui->mipLevel->setEnabled(false);
  else
    ui->mipLevel->setEnabled(true);

  ui->sliceFace->clear();

  uint32_t numSlices = 1;

  if(tex.arraysize == 1 && tex.depth <= 1)
  {
    ui->sliceFace->setEnabled(false);
  }
  else
  {
    ui->sliceFace->setEnabled(true);

    QString cubeFaces[] = {lit("X+"), lit("X-"), lit("Y+"), lit("Y-"), lit("Z+"), lit("Z-")};

    numSlices = tex.arraysize;

    // for 3D textures, display the number of slices at this mip
    if(tex.depth > 1)
      numSlices = qMax(1u, tex.depth >> (int)ui->mipLevel->currentIndex());

    for(uint32_t i = 0; i < numSlices; i++)
    {
      if(tex.cubemap)
      {
        QString name = cubeFaces[i % 6];
        if(numSlices > 6)
          name = QFormatStr("[%1] %2").arg(i / 6).arg(
              cubeFaces[i % 6]);    // Front 1, Back 2, 3, 4 etc for cube arrays
        ui->sliceFace->addItem(name);
      }
      else
      {
        ui->sliceFace->addItem(tr("Slice %1").arg(i));
      }
    }
  }

  // enable signals for mipLevel and sliceFace
  ui->mipLevel->blockSignals(false);
  ui->sliceFace->blockSignals(false);

  {
    int highestMip = -1;

    // only switch to the selected mip for outputs, and when changing action
    if(!currentTextureIsLocked() && m_Following.Type != FollowType::ReadOnly && (newAction || newtex))
      highestMip = m_Following.GetHighestMip(m_Ctx);

    // assuming we get a valid mip for the highest mip, only switch to it
    // if we've selected a new texture, or if it's different than the last mip.
    // This prevents the case where the user has clicked on another mip and
    // we don't want to snap their view back when stepping between events with the
    // same mip used. But it does mean that if they are stepping between
    // events with different mips used, then we will update in that case.
    if(highestMip >= 0 && (newtex || highestMip != m_PrevHighestMip))
    {
      usemipsettings = false;
      ui->mipLevel->setCurrentIndex(qBound(0, highestMip, (int)tex.mips - 1));
    }

    if(ui->mipLevel->currentIndex() == -1)
      ui->mipLevel->setCurrentIndex(qBound(0, m_PrevHighestMip, (int)tex.mips - 1));

    m_PrevHighestMip = highestMip;
  }

  {
    int firstArraySlice = -1;
    // only switch to the selected mip for outputs, and when changing action
    if(!currentTextureIsLocked() && m_Following.Type != FollowType::ReadOnly && (newAction || newtex))
      firstArraySlice = m_Following.GetFirstArraySlice(m_Ctx);

    // see above with highestMip and prevHighestMip for the logic behind this
    if(firstArraySlice >= 0 && (newtex || firstArraySlice != m_PrevFirstArraySlice))
    {
      useslicesettings = false;
      ui->sliceFace->setCurrentIndex(qBound(0, firstArraySlice, (int)numSlices - 1));
    }

    if(ui->sliceFace->currentIndex() == -1)
      ui->sliceFace->setCurrentIndex(qBound(0, m_PrevFirstArraySlice, (int)numSlices - 1));

    m_PrevFirstArraySlice = firstArraySlice;
  }

  // because slice and mip are specially set above, we restore any per-tex settings to apply
  // even if we don't switch to a new texture.
  // Note that if the slice or mip was changed because that slice or mip is the selected one
  // at the API level, we leave this alone.
  if(m_Ctx.Config().TextureViewer_PerTexSettings && m_TextureSettings.contains(tex.resourceId))
  {
    if(usemipsettings)
      ui->mipLevel->setCurrentIndex(m_TextureSettings[tex.resourceId].mip);

    if(useslicesettings)
      ui->sliceFace->setCurrentIndex(m_TextureSettings[tex.resourceId].slice);

    if(m_Ctx.Config().TextureViewer_PerTexYFlip)
      ui->flip_y->setChecked(m_TextureSettings[tex.resourceId].flip_y);
  }

  // handling for if we've switched to a new texture
  if(newtex)
  {
    // if we save certain settings per-texture, restore them (if we have any)
    if(m_Ctx.Config().TextureViewer_PerTexSettings && m_TextureSettings.contains(tex.resourceId))
    {
      ui->channels->setCurrentIndex(m_TextureSettings[tex.resourceId].displayType);

      ui->customShader->setCurrentText(m_TextureSettings[tex.resourceId].customShader);

      ui->channelRed->setChecked(m_TextureSettings[tex.resourceId].r);
      ui->channelGreen->setChecked(m_TextureSettings[tex.resourceId].g);
      ui->channelBlue->setChecked(m_TextureSettings[tex.resourceId].b);
      ui->channelAlpha->setChecked(m_TextureSettings[tex.resourceId].a);

      ui->depthDisplay->setChecked(m_TextureSettings[tex.resourceId].depth);
      ui->stencilDisplay->setChecked(m_TextureSettings[tex.resourceId].stencil);

      if(m_Ctx.Config().TextureViewer_PerTexYFlip)
        ui->flip_y->setChecked(m_TextureSettings[tex.resourceId].flip_y);

      m_NoRangePaint = true;
      ui->rangeHistogram->setRange(m_TextureSettings[m_TexDisplay.resourceId].minrange,
                                   m_TextureSettings[m_TexDisplay.resourceId].maxrange);
      m_NoRangePaint = false;
    }
    else if(m_Ctx.Config().TextureViewer_PerTexSettings)
    {
      // if we are using per-tex settings, reset back to RGB
      ui->channels->setCurrentIndex(0);

      ui->customShader->setCurrentText(QString());

      ui->channelRed->setChecked(true);
      ui->channelGreen->setChecked(true);
      ui->channelBlue->setChecked(true);
      ui->channelAlpha->setChecked(false);

      // for alpha textures, only show the alpha channel
      if(texptr->format.type == ResourceFormatType::A8)
      {
        ui->channelRed->setChecked(false);
        ui->channelGreen->setChecked(false);
        ui->channelBlue->setChecked(false);
        ui->channelAlpha->setChecked(true);
      }

      ui->depthDisplay->setChecked(true);
      ui->stencilDisplay->setChecked(false);

      if(m_Ctx.Config().TextureViewer_PerTexYFlip)
        ui->flip_y->setChecked(false);

      m_NoRangePaint = true;
      UI_SetHistogramRange(texptr, m_TexDisplay.typeCast);
      m_NoRangePaint = false;
    }

    // reset the range if desired
    if(m_Ctx.Config().TextureViewer_ResetRange)
    {
      UI_SetHistogramRange(texptr, m_TexDisplay.typeCast);
    }
  }

  UI_UpdateFittedScale();
  UI_UpdateTextureDetails();
  UI_UpdateChannels();

  if(ui->autoFit->isChecked())
    AutoFitRange();

  m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
    RT_UpdateVisualRange(r);

    RT_UpdateAndDisplay(r);

    if(m_Output != NULL)
      RT_PickPixelsAndUpdate(r);
  });

  HighlightUsage();
}

void TextureViewer::UI_SetHistogramRange(const TextureDescription *tex, CompType typeCast)
{
  if(tex != NULL && (tex->format.compType == CompType::SNorm || typeCast == CompType::SNorm))
    ui->rangeHistogram->setRange(-1.0f, 1.0f);
  else
    ui->rangeHistogram->setRange(0.0f, 1.0f);
}

void TextureViewer::UI_UpdateChannels()
{
  TextureDescription *tex = GetCurrentTexture();

#define SHOW(widget) widget->setVisible(true)
#define HIDE(widget) widget->setVisible(false)
#define ENABLE(widget) widget->setEnabled(true)
#define DISABLE(widget) widget->setEnabled(false)

  if(tex != NULL && (tex->creationFlags & TextureCategory::SwapBuffer))
  {
    // swapbuffer is always srgb for 8-bit types, linear for 16-bit types
    DISABLE(ui->gammaDisplay);

    if(tex->format.compByteWidth == 2 && tex->format.type == ResourceFormatType::Regular)
      m_TexDisplay.linearDisplayAsGamma = false;
    else
      m_TexDisplay.linearDisplayAsGamma = true;
  }
  else
  {
    if(tex != NULL && !tex->format.SRGBCorrected())
      ENABLE(ui->gammaDisplay);
    else
      DISABLE(ui->gammaDisplay);

    m_TexDisplay.linearDisplayAsGamma =
        !ui->gammaDisplay->isEnabled() || ui->gammaDisplay->isChecked();
  }

  if(tex != NULL && tex->format.SRGBCorrected())
    m_TexDisplay.linearDisplayAsGamma = false;

  bool dsv = false;
  if(tex != NULL)
    dsv = (tex->creationFlags & TextureCategory::DepthTarget) ||
          (tex->format.compType == CompType::Depth);

  bool yuv = false;
  if(tex != NULL)
    yuv = (tex->format.type == ResourceFormatType::YUV8 ||
           tex->format.type == ResourceFormatType::YUV10 ||
           tex->format.type == ResourceFormatType::YUV12 ||
           tex->format.type == ResourceFormatType::YUV16);

  const bool defaultDisplay = ui->channels->currentIndex() == 0;
  const bool rgbmDisplay = ui->channels->currentIndex() == 1;
  const bool yuvDecodeDisplay = ui->channels->currentIndex() == 2;
  const bool customDisplay = ui->channels->currentIndex() == 3;

  if(customDisplay && m_NeedCustomReload)
  {
    m_NeedCustomReload = false;

    reloadCustomShaders(QString());
  }

  ui->channels->setItemText(0, yuv ? lit("YUVA") : lit("RGBA"));
  ui->channelRed->setText(lit("R"));
  ui->channelGreen->setText(lit("G"));
  ui->channelBlue->setText(lit("B"));

  if(dsv && !customDisplay)
  {
    // Depth display (when not using custom)

    HIDE(ui->channelRed);
    HIDE(ui->channelGreen);
    HIDE(ui->channelBlue);
    HIDE(ui->channelAlpha);
    HIDE(ui->mulSep);
    HIDE(ui->mulLabel);
    HIDE(ui->hdrMul);
    HIDE(ui->customShader);
    HIDE(ui->customCreate);
    HIDE(ui->customEdit);
    HIDE(ui->customDelete);
    SHOW(ui->depthDisplay);
    SHOW(ui->stencilDisplay);

    m_TexDisplay.red = ui->depthDisplay->isChecked();
    m_TexDisplay.green = ui->stencilDisplay->isChecked();
    m_TexDisplay.blue = false;
    m_TexDisplay.alpha = false;

    if(m_TexDisplay.red == m_TexDisplay.green && !m_TexDisplay.red)
    {
      m_TexDisplay.red = true;
      ui->depthDisplay->setChecked(true);
    }

    m_TexDisplay.decodeYUV = false;
    m_TexDisplay.hdrMultiplier = -1.0f;
    if(m_TexDisplay.customShaderId != ResourceId())
    {
      m_CurPixelValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      m_CurRealValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      UI_UpdateStatusText();
    }
    m_TexDisplay.customShaderId = ResourceId();
  }
  else if(defaultDisplay || yuvDecodeDisplay || !m_Ctx.IsCaptureLoaded())
  {
    // RGBA. YUV Decode is almost identical but we set decodeYUV
    SHOW(ui->channelRed);
    SHOW(ui->channelGreen);
    SHOW(ui->channelBlue);
    SHOW(ui->channelAlpha);
    HIDE(ui->mulSep);
    HIDE(ui->mulLabel);
    HIDE(ui->hdrMul);
    HIDE(ui->customShader);
    HIDE(ui->customCreate);
    HIDE(ui->customEdit);
    HIDE(ui->customDelete);
    HIDE(ui->depthDisplay);
    HIDE(ui->stencilDisplay);

    m_TexDisplay.red = ui->channelRed->isChecked();
    m_TexDisplay.green = ui->channelGreen->isChecked();
    m_TexDisplay.blue = ui->channelBlue->isChecked();
    m_TexDisplay.alpha = ui->channelAlpha->isChecked();

    if(!yuvDecodeDisplay && yuv)
    {
      ui->channelRed->setText(lit("V"));
      ui->channelGreen->setText(lit("Y"));
      ui->channelBlue->setText(lit("U"));
    }

    m_TexDisplay.decodeYUV = yuvDecodeDisplay;
    m_TexDisplay.hdrMultiplier = -1.0f;
    if(m_TexDisplay.customShaderId != ResourceId())
    {
      m_CurPixelValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      m_CurRealValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      UI_UpdateStatusText();
    }
    m_TexDisplay.customShaderId = ResourceId();
  }
  else if(rgbmDisplay)
  {
    // RGBM
    SHOW(ui->channelRed);
    SHOW(ui->channelGreen);
    SHOW(ui->channelBlue);
    HIDE(ui->channelAlpha);
    SHOW(ui->mulSep);
    SHOW(ui->mulLabel);
    SHOW(ui->hdrMul);
    HIDE(ui->customShader);
    HIDE(ui->customCreate);
    HIDE(ui->customEdit);
    HIDE(ui->customDelete);
    HIDE(ui->depthDisplay);
    HIDE(ui->stencilDisplay);

    m_TexDisplay.red = ui->channelRed->isChecked();
    m_TexDisplay.green = ui->channelGreen->isChecked();
    m_TexDisplay.blue = ui->channelBlue->isChecked();
    m_TexDisplay.alpha = false;

    bool ok = false;
    float mul = ui->hdrMul->currentText().toFloat(&ok);

    if(!ok)
    {
      mul = 32.0f;
      ui->hdrMul->setCurrentText(lit("32"));
    }

    m_TexDisplay.decodeYUV = false;
    m_TexDisplay.hdrMultiplier = mul;
    if(m_TexDisplay.customShaderId != ResourceId())
    {
      m_CurPixelValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      m_CurRealValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
      UI_UpdateStatusText();
    }
    m_TexDisplay.customShaderId = ResourceId();
  }
  else if(customDisplay)
  {
    // custom shaders
    SHOW(ui->channelRed);
    SHOW(ui->channelGreen);
    SHOW(ui->channelBlue);
    SHOW(ui->channelAlpha);
    HIDE(ui->mulSep);
    HIDE(ui->mulLabel);
    HIDE(ui->hdrMul);
    SHOW(ui->customShader);
    SHOW(ui->customCreate);
    SHOW(ui->customEdit);
    SHOW(ui->customDelete);
    HIDE(ui->depthDisplay);
    HIDE(ui->stencilDisplay);

    m_TexDisplay.red = ui->channelRed->isChecked();
    m_TexDisplay.green = ui->channelGreen->isChecked();
    m_TexDisplay.blue = ui->channelBlue->isChecked();
    m_TexDisplay.alpha = ui->channelAlpha->isChecked();

    m_TexDisplay.decodeYUV = false;
    m_TexDisplay.hdrMultiplier = -1.0f;

    m_TexDisplay.customShaderId = ResourceId();

    QString shaderName = ui->customShader->currentText().toUpper();

    if(m_CustomShaders.contains(shaderName))
    {
      if(m_TexDisplay.customShaderId == ResourceId())
      {
        m_CurPixelValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
        m_CurRealValue.floatValue = {0.0f, 0.0f, 0.0f, 0.0f};
        UI_UpdateStatusText();
      }
      m_TexDisplay.customShaderId = m_CustomShaders[shaderName];
      ui->customDelete->setEnabled(true);
      ui->customEdit->setEnabled(true);
    }
    else
    {
      ui->customDelete->setEnabled(false);
      ui->customEdit->setEnabled(false);
    }
  }

#undef HIDE
#undef SHOW
#undef ENABLE
#undef DISABLE

  m_TexDisplay.flipY = ui->flip_y->isChecked();

  INVOKE_MEMFN(RT_UpdateAndDisplay);
  INVOKE_MEMFN(RT_UpdateVisualRange);
}

void TextureViewer::SetupTextureTabs()
{
  ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

  if(!textureTabs)
    return;

  QIcon tabIcon;
  tabIcon.addFile(QStringLiteral(":/logo.svg"), QSize(), QIcon::Normal, QIcon::Off);

  textureTabs->setTabIcon(0, tabIcon);

  textureTabs->setElideMode(Qt::ElideRight);

  QObject::connect(textureTabs, &QTabWidget::currentChanged, this,
                   &TextureViewer::textureTab_Changed);
  QObject::connect(textureTabs, &QTabWidget::tabCloseRequested, this,
                   &TextureViewer::textureTab_Closing);

  textureTabs->disableUserDrop();

  textureTabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);

  QObject::connect(textureTabs->tabBar(), &QTabBar::customContextMenuRequested, this,
                   &TextureViewer::textureTab_Menu);

  // show any fixed panels that got closed by previous bugs and saved as closed

  if(ui->dockarea->areaOf(ui->inputThumbs) == NULL)
    ui->dockarea->moveToolWindow(
        ui->inputThumbs,
        ToolWindowManager::AreaReference(ToolWindowManager::RightOf,
                                         ui->dockarea->areaOf(ui->renderContainer), 0.25f));

  if(ui->dockarea->areaOf(ui->outputThumbs) == NULL)
    ui->dockarea->moveToolWindow(
        ui->outputThumbs, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
                                                           ui->dockarea->areaOf(ui->inputThumbs)));

  if(ui->dockarea->areaOf(ui->pixelContextLayout) == NULL)
    ui->dockarea->moveToolWindow(
        ui->pixelContextLayout,
        ToolWindowManager::AreaReference(ToolWindowManager::BottomOf,
                                         ui->dockarea->areaOf(ui->outputThumbs), 0.25f));

  ui->renderContainer->setLayout(ui->renderLayout);
}

void TextureViewer::RemoveTextureTabs(int firstIndex)
{
  ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

  // remove all tabs from firstIndex, except unclosable tabs
  for(int i = firstIndex; i < textureTabs->count();)
  {
    if(ui->dockarea->toolWindowProperties(textureTabs->widget(i)) & ToolWindowManager::HideCloseButton)
    {
      i++;
      continue;
    }
    textureTabs->removeTab(i);
  }
}

void TextureViewer::textureTab_Menu(const QPoint &pos)
{
  ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

  int tabIndex = textureTabs->tabBar()->tabAt(pos);

  if(tabIndex == -1)
    return;

  QAction closeTab(tr("Close tab"), this);
  QAction closeOtherTabs(tr("Close other tabs"), this);
  QAction closeRightTabs(tr("Close tabs to the right"), this);

  if(ui->dockarea->toolWindowProperties(textureTabs->widget(tabIndex)) &
     ToolWindowManager::HideCloseButton)
    closeTab.setEnabled(false);

  QMenu contextMenu(this);

  contextMenu.addAction(&closeTab);
  contextMenu.addAction(&closeOtherTabs);
  contextMenu.addAction(&closeRightTabs);

  QObject::connect(&closeTab, &QAction::triggered, [textureTabs, tabIndex]() {
    // remove the tab at this index
    textureTabs->removeTab(tabIndex);
  });

  QObject::connect(&closeRightTabs, &QAction::triggered,
                   [this, tabIndex]() { RemoveTextureTabs(tabIndex + 1); });

  QObject::connect(&closeOtherTabs, &QAction::triggered, [this]() { RemoveTextureTabs(0); });

  RDDialog::show(&contextMenu, QCursor::pos());
}

void TextureViewer::textureTab_Changed(int index)
{
  ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

  if(!textureTabs)
    return;

  QWidget *w = textureTabs->widget(index);

  if(w)
  {
    w->setLayout(ui->renderLayout);

    if(w == ui->renderContainer)
      m_LockedId = ResourceId();
    else
      m_LockedId = w->property("id").value<ResourceId>();

    UI_UpdateCachedTexture();
  }

  UI_OnTextureSelectionChanged(false);
}

void TextureViewer::textureTab_Closing(int index)
{
  ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);
  if(index > 0)
  {
    // this callback happens AFTER the widget has already been removed unfortunately, so
    // we need to search through the locked tab list to see which one was removed to be
    // able to delete it properly.
    QList<ResourceId> ids = m_LockedTabs.keys();
    for(int i = 0; i < textureTabs->count(); i++)
    {
      QWidget *w = textureTabs->widget(i);
      ResourceId id = w->property("id").value<ResourceId>();
      ids.removeOne(id);
    }

    if(ids.count() != 1)
      qWarning() << "Expected only one removed tab, got " << ids.count();

    for(ResourceId id : ids)
      m_LockedTabs.remove(id);

    return;
  }

  // should never get here - tab 0 is the dynamic tab which is uncloseable.
  qCritical() << "Somehow closing dynamic tab?";
  if(textureTabs->count() > 1)
  {
    textureTabs->setCurrentIndex(0);
    textureTabs->widget(0)->show();
  }
}

ResourcePreview *TextureViewer::UI_CreateThumbnail(ThumbnailStrip *strip)
{
  ResourcePreview *prev = new ResourcePreview(m_Ctx, m_Output);

  QObject::connect(prev, &ResourcePreview::clicked, this, &TextureViewer::thumb_clicked);
  QObject::connect(prev, &ResourcePreview::doubleClicked, this, &TextureViewer::thumb_doubleClicked);

  prev->setActive(false);
  strip->addThumb(prev);
  return prev;
}

void TextureViewer::UI_CreateThumbnails()
{
  if(!ui->outputThumbs->thumbs().isEmpty())
    return;

  // these will expand, but we make sure that there is a good set reserved
  for(int i = 0; i < 9; i++)
  {
    ResourcePreview *prev = UI_CreateThumbnail(ui->outputThumbs);

    if(i == 0)
      prev->setSelected(true);
  }

  for(int i = 0; i < 128; i++)
    UI_CreateThumbnail(ui->inputThumbs);
}

void TextureViewer::ViewTexture(ResourceId ID, CompType typeCast, bool focus)
{
  if(QThread::currentThread() != QCoreApplication::instance()->thread())
  {
    GUIInvoke::call(this, [this, ID, typeCast, focus] { this->ViewTexture(ID, typeCast, focus); });
    return;
  }

  if(typeCast != CompType::Typeless)
    m_TextureSettings[ID].typeCast = typeCast;

  if(m_LockedTabs.contains(ID))
  {
    if(focus)
      ToolWindowManager::raiseToolWindow(this);

    QWidget *w = m_LockedTabs[ID];
    ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

    int idx = textureTabs->indexOf(w);

    if(idx >= 0)
      textureTabs->setCurrentIndex(idx);

    INVOKE_MEMFN(RT_UpdateAndDisplay);
    return;
  }

  TextureDescription *tex = m_Ctx.GetTexture(ID);
  if(tex)
  {
    QWidget *lockedContainer = new QWidget(this);
    lockedContainer->setWindowTitle(m_Ctx.GetResourceName(ID));
    lockedContainer->setProperty("id", QVariant::fromValue(ID));

    ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

    ToolWindowManager::AreaReference ref(ToolWindowManager::AddTo, textureTabs);

    ui->dockarea->addToolWindow(lockedContainer, ref);
    ui->dockarea->setToolWindowProperties(
        lockedContainer,
        ToolWindowManager::DisallowUserDocking | ToolWindowManager::AlwaysDisplayFullTabs);

    lockedContainer->setLayout(ui->renderLayout);

    int idx = textureTabs->indexOf(lockedContainer);

    if(idx >= 0)
      textureTabs->setTabIcon(idx, Icons::page_go());
    else
      qCritical() << "Couldn't get tab index of new tab to set icon";

    // newPanel.DockHandler.TabPageContextMenuStrip = tabContextMenu;

    if(focus)
      ToolWindowManager::raiseToolWindow(this);

    m_LockedTabs[ID] = lockedContainer;

    INVOKE_MEMFN(RT_UpdateAndDisplay);
    return;
  }

  BufferDescription *buf = m_Ctx.GetBuffer(ID);
  if(buf)
  {
    IBufferViewer *viewer = m_Ctx.ViewBuffer(0, ~0ULL, ID);

    m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
  }
}

void TextureViewer::ViewFollowedResource(FollowType followType, ShaderStage stage, int32_t index,
                                         int32_t arrayElement)
{
  Following f;
  f.Type = followType;
  f.Stage = stage;
  f.index = index;
  f.arrayEl = arrayElement;

  if(f.Type == FollowType::OutputColor || f.Type == FollowType::OutputDepth)
  {
    f.Stage = ShaderStage::Pixel;
    f.arrayEl = 0;
  }

  if(f.Type == FollowType::OutputDepth)
    f.index = 0;

  for(ResourcePreview *p :
      (f.Type == FollowType::ReadOnly ? ui->inputThumbs->thumbs() : ui->outputThumbs->thumbs()))
  {
    Following follow = p->property("f").value<Following>();

    if(follow == f)
    {
      SelectPreview(p);

      ToolWindowManagerArea *textureTabs = ui->dockarea->areaOf(ui->renderContainer);

      // following tab is always tab 0.
      textureTabs->setCurrentIndex(0);

      return;
    }
  }

  qWarning() << "Couldn't find matching bound resource";
}

ResourceId TextureViewer::GetCurrentResource()
{
  TextureDescription *tex = GetCurrentTexture();
  if(tex)
    return tex->resourceId;
  return ResourceId();
}

Subresource TextureViewer::GetSelectedSubresource()
{
  return m_TexDisplay.subresource;
}

void TextureViewer::SetSelectedSubresource(Subresource sub)
{
  TextureDescription *tex = GetCurrentTexture();
  if(!tex)
    return;

  if(tex->mips > 1)
    ui->mipLevel->setCurrentIndex(qMin(sub.mip, tex->mips - 1));
  else if(tex->msSamp > 1)
    ui->mipLevel->setCurrentIndex(qMin(sub.sample, tex->msSamp - 1));

  if(tex->depth > 1)
    ui->sliceFace->setCurrentIndex(qMin(sub.slice, tex->depth - 1));
  else
    ui->sliceFace->setCurrentIndex(qMin(sub.slice, tex->arraysize - 1));
}

void TextureViewer::GotoLocation(uint32_t x, uint32_t y)
{
  if(!m_Ctx.IsCaptureLoaded())
    return;

  TextureDescription *tex = GetCurrentTexture();

  if(tex == NULL)
    return;

  x = qMin(x << m_TexDisplay.subresource.mip, uint32_t(tex->width - 1));
  y = qMin(y << m_TexDisplay.subresource.mip, uint32_t(tex->height - 1));

  m_PickedPoint = QPoint(x, y);

  if(ShouldFlipForGL())
    m_PickedPoint.setY((int)(tex->height - 1) - m_PickedPoint.y());
  if(m_TexDisplay.flipY)
    m_PickedPoint.setY((int)(tex->height - 1) - m_PickedPoint.y());

  // centre the picked point.
  QPoint scrollPos;
  scrollPos.setX(-m_PickedPoint.x() * m_TexDisplay.scale + realRenderWidth() / 2);
  scrollPos.setY(-m_PickedPoint.y() * m_TexDisplay.scale + realRenderHeight() / 2);

  setScrollPosition(scrollPos);

  if(m_Output != NULL)
    INVOKE_MEMFN(RT_PickPixelsAndUpdate);
  INVOKE_MEMFN(RT_UpdateAndDisplay);

  UI_UpdateStatusText();
}

DebugOverlay TextureViewer::GetTextureOverlay()
{
  return m_TexDisplay.overlay;
}

void TextureViewer::SetTextureOverlay(DebugOverlay overlay)
{
  ui->overlay->setCurrentIndex((int)overlay);
}

bool TextureViewer::IsZoomAutoFit()
{
  return ui->fitToWindow->isChecked();
}

float TextureViewer::GetZoomLevel()
{
  if(ui->fitToWindow->isChecked())
    return m_TexDisplay.scale;

  QString zoomText = ui->zoomOption->currentText().replace(QLatin1Char('%'), QLatin1Char(' '));

  bool ok = false;
  int zoom = zoomText.toInt(&ok);

  if(!ok)
    zoom = 100;

  return (float)(zoom) / 100.0f;
}

void TextureViewer::SetZoomLevel(bool autofit, float zoom)
{
  ui->fitToWindow->setChecked(autofit);
  if(!autofit)
    UI_SetScale(zoom);
}

rdcpair<float, float> TextureViewer::GetHistogramRange()
{
  return {m_TexDisplay.rangeMin, m_TexDisplay.rangeMax};
}

void TextureViewer::SetHistogramRange(float blackpoint, float whitepoint)
{
  ui->rangeHistogram->setRange(blackpoint, whitepoint);
}

uint32_t TextureViewer::GetChannelVisibilityBits()
{
  uint32_t ret = 0;
  if(m_TexDisplay.red)
    ret |= 0x1;
  if(m_TexDisplay.green)
    ret |= 0x2;
  if(m_TexDisplay.blue)
    ret |= 0x4;
  if(m_TexDisplay.alpha)
    ret |= 0x8;
  return ret;
}

void TextureViewer::SetChannelVisibility(bool red, bool green, bool blue, bool alpha)
{
  ui->channelRed->setChecked(red);
  ui->channelGreen->setChecked(green);
  ui->channelBlue->setChecked(blue);
  ui->channelAlpha->setChecked(alpha);
}

void TextureViewer::texContextItem_triggered()
{
  QAction *act = qobject_cast<QAction *>(QObject::sender());

  QVariant eid = act->property("eid");
  if(eid.isValid())
  {
    m_Ctx.SetEventID({}, eid.toUInt(), eid.toUInt());
    return;
  }

  QVariant idvar = act->property("id");
  if(idvar.isValid())
  {
    ResourceId id = idvar.value<ResourceId>();
    CompType typeCast = CompType::Typeless;
    if(m_TextureSettings.contains(id))
      typeCast = m_TextureSettings[id].typeCast;

    ViewTexture(id, typeCast, false);
    return;
  }
}

void TextureViewer::AddResourceUsageEntry(QMenu &menu, uint32_t start, uint32_t end,
                                          ResourceUsage usage)
{
  QAction *item = NULL;

  if(start == end)
    item = new QAction(
        QFormatStr("EID %1: %2").arg(start).arg(ToQStr(usage, m_Ctx.APIProps().pipelineType)), this);
  else
    item = new QAction(
        QFormatStr("EID %1-%2: %3").arg(start).arg(end).arg(ToQStr(usage, m_Ctx.APIProps().pipelineType)),
        this);

  QObject::connect(item, &QAction::triggered, this, &TextureViewer::texContextItem_triggered);
  item->setProperty("eid", QVariant(end));

  menu.addAction(item);
}

void TextureViewer::OpenResourceContextMenu(ResourceId id, bool input,
                                            const rdcarray<EventUsage> &usage)
{
  QMenu contextMenu(this);

  QAction openLockedTab(tr("Open new Locked Tab"), this);
  QAction openResourceInspector(tr("Open in Resource Inspector"), this);
  QAction usageTitle(tr("Used:"), this);
  QAction imageLayout(this);

  openLockedTab.setIcon(Icons::action_hover());
  openResourceInspector.setIcon(Icons::link());

  if(m_Ctx.CurPipelineState().SupportsBarriers())
  {
    imageLayout.setText(tr("Image is in layout ") + m_Ctx.CurPipelineState().GetResourceLayout(id));
    contextMenu.addAction(&imageLayout);
    contextMenu.addSeparator();
  }

  if(id != ResourceId())
  {
    contextMenu.addAction(&openLockedTab);
    contextMenu.addAction(&openResourceInspector);

    contextMenu.addSeparator();
    m_Ctx.Extensions().MenuDisplaying(input ? ContextMenu::TextureViewer_InputThumbnail
                                            : ContextMenu::TextureViewer_OutputThumbnail,
                                      &contextMenu, {{"resourceId", id}});

    contextMenu.addSeparator();
    contextMenu.addAction(&usageTitle);

    openLockedTab.setProperty("id", QVariant::fromValue(id));

    QObject::connect(&openLockedTab, &QAction::triggered, this,
                     &TextureViewer::texContextItem_triggered);

    QObject::connect(&openResourceInspector, &QAction::triggered, [this, id]() {
      m_Ctx.ShowResourceInspector();

      m_Ctx.GetResourceInspector()->Inspect(id);
    });

    CombineUsageEvents(m_Ctx, usage,
                       [this, &contextMenu](uint32_t start, uint32_t end, ResourceUsage use) {
                         AddResourceUsageEntry(contextMenu, start, end, use);
                       });

    RDDialog::show(&contextMenu, QCursor::pos());
  }
  else
  {
    m_Ctx.Extensions().MenuDisplaying(input ? ContextMenu::TextureViewer_InputThumbnail
                                            : ContextMenu::TextureViewer_OutputThumbnail,
                                      &contextMenu, {});

    RDDialog::show(&contextMenu, QCursor::pos());
  }
}

void TextureViewer::InitResourcePreview(ResourcePreview *prev, BoundResource res, bool force,
                                        Following &follow, const QString &bindName,
                                        const QString &slotName)
{
  if(res.resourceId != ResourceId() || force)
  {
    QString fullname = bindName;
    if(!m_Ctx.IsAutogeneratedName(res.resourceId))
    {
      if(!fullname.isEmpty())
        fullname += lit(" = ");
      fullname += m_Ctx.GetResourceName(res.resourceId);
    }
    if(fullname.isEmpty())
      fullname = m_Ctx.GetResourceName(res.resourceId);

    prev->setResourceName(fullname);

    WindowingData winData = prev->GetWidgetWindowingData();

    prev->setProperty("f", QVariant::fromValue(follow));
    prev->setSlotName(slotName);
    prev->setActive(true);
    prev->setSelected(m_Following == follow);

    if(m_Ctx.GetTexture(res.resourceId))
    {
      m_Ctx.Replay().AsyncInvoke([this, winData, res](IReplayController *) {
        Subresource sub = {0, 0, ~0U};
        if(res.firstMip >= 0)
          sub.mip = res.firstMip;
        if(res.firstSlice >= 0)
          sub.slice = res.firstSlice;
        m_Output->AddThumbnail(winData, res.resourceId, sub, res.typeCast);
      });
    }
    else
    {
      m_Ctx.Replay().AsyncInvoke([this, winData](IReplayController *) {
        m_Output->AddThumbnail(winData, ResourceId(), {0, 0, ~0U}, CompType::Typeless);
      });
    }
  }
  else if(m_Following == follow)
  {
    prev->setResourceName(tr("Unused"));
    prev->setActive(true);
    prev->setSelected(true);

    WindowingData winData = prev->GetWidgetWindowingData();
    m_Ctx.Replay().AsyncInvoke([this, winData](IReplayController *) {
      m_Output->AddThumbnail(winData, ResourceId(), {0, 0, ~0U}, CompType::Typeless);
    });
  }
  else
  {
    prev->setResourceName(QString());
    prev->setActive(false);
    prev->setSelected(false);
  }
}

void TextureViewer::InitStageResourcePreviews(ShaderStage stage,
                                              const rdcarray<ShaderResource> &resourceDetails,
                                              const rdcarray<Bindpoint> &mapping,
                                              rdcarray<BoundResourceArray> &ResList,
                                              ThumbnailStrip *prevs, int &prevIndex, bool copy,
                                              bool rw)
{
  for(int idx = 0; idx < mapping.count(); idx++)
  {
    const Bindpoint &key = mapping[idx];

    const rdcarray<BoundResource> *resArray = NULL;
    uint32_t dynamicallyUsedResCount = 1;
    int32_t firstIndex = 0;

    int residx = ResList.indexOf(key);
    if(residx >= 0)
    {
      resArray = &ResList[residx].resources;
      dynamicallyUsedResCount = ResList[residx].dynamicallyUsedCount;
      firstIndex = ResList[residx].firstIndex;
    }

    int arrayLen = resArray != NULL ? resArray->count() : 1;

    const bool collapseArray = arrayLen > 8 && dynamicallyUsedResCount > 20;

    for(int i = 0; i < arrayLen; i++)
    {
      int arrayIdx = firstIndex + i;

      if(resArray && i >= resArray->count())
        break;

      if(resArray && !resArray->at(i).dynamicallyUsed)
        continue;

      BoundResource res = {};

      if(resArray)
        res = resArray->at(i);

      Following follow(*this, rw ? FollowType::ReadWrite : FollowType::ReadOnly, stage, idx,
                       arrayIdx);

      // show if it's referenced by the shader - regardless of empty or not
      bool show = key.used || copy;

      // omit buffers even if the shader uses them.
      if(res.resourceId != ResourceId() && m_Ctx.GetTexture(res.resourceId) == NULL)
        show = copy;

      // it's the one we're following
      show = show || (follow == m_Following);

      ResourcePreview *prev = NULL;

      if(prevIndex < prevs->thumbs().size())
      {
        prev = prevs->thumbs()[prevIndex];

        // don't use it if we're not actually going to show it
        if(!show && !prev->isActive())
          continue;
      }
      else
      {
        // don't create it if we're not actually going to show it
        if(!show)
          continue;

        prev = UI_CreateThumbnail(prevs);
      }

      prevIndex++;

      QString slotName = QFormatStr("%1 %2%3")
                             .arg(m_Ctx.CurPipelineState().Abbrev(stage))
                             .arg(rw ? lit("RW ") : lit(""))
                             .arg(idx);

      if(collapseArray)
        slotName += QFormatStr(" Arr[%1]").arg(arrayLen);
      else
        slotName += QFormatStr("[%1]").arg(arrayIdx);

      if(copy)
        slotName = tr("SRC");

      QString bindName;

      for(const ShaderResource &bind : resourceDetails)
      {
        if(bind.bindPoint == idx)
        {
          bindName = bind.name;
          break;
        }
      }

      if(copy)
        bindName = tr("Source");

      InitResourcePreview(prev, show ? res : BoundResource(), show, follow, bindName, slotName);

      if(collapseArray)
        break;
    }
  }
}

void TextureViewer::thumb_doubleClicked(QMouseEvent *e)
{
  if(e->buttons() & Qt::LeftButton)
  {
    ResourceId id = m_Following.GetResourceId(m_Ctx);

    if(id != ResourceId())
    {
      ViewTexture(id, m_Following.GetTypeHint(m_Ctx), false);
    }
  }
}

void TextureViewer::thumb_clicked(QMouseEvent *e)
{
  if(e->buttons() & Qt::LeftButton)
  {
    ResourcePreview *prev = qobject_cast<ResourcePreview *>(QObject::sender());

    SelectPreview(prev);
  }

  if(e->buttons() & Qt::RightButton)
  {
    ResourcePreview *prev = qobject_cast<ResourcePreview *>(QObject::sender());

    Following follow = prev->property("f").value<Following>();

    ResourceId id = follow.GetResourceId(m_Ctx);

    if(id == ResourceId() && follow == m_Following)
      id = m_TexDisplay.resourceId;

    rdcarray<EventUsage> empty;

    bool input = follow.Type == FollowType::ReadOnly;

    if(id == ResourceId())
    {
      OpenResourceContextMenu(id, input, empty);
    }
    else
    {
      m_Ctx.Replay().AsyncInvoke([this, id, input](IReplayController *r) {
        rdcarray<EventUsage> usage = r->GetUsage(id);

        GUIInvoke::call(this,
                        [this, id, input, usage]() { OpenResourceContextMenu(id, input, usage); });
      });
    }
  }
}

void TextureViewer::render_mouseWheel(QWheelEvent *e)
{
  QPoint cursorPos = e->pos();

  setFitToWindow(false);

  // scroll in logarithmic scale
  double logScale = logf(m_TexDisplay.scale);
  logScale += e->delta() / 2500.0;
  UI_SetScale((float)expf(logScale), cursorPos.x() * ui->render->devicePixelRatioF(),
              cursorPos.y() * ui->render->devicePixelRatioF());

  e->accept();
}

void TextureViewer::render_mouseMove(QMouseEvent *e)
{
  if(m_Output == NULL)
    return;

  m_CurHoverPixel.setX(int((float(e->x() * ui->render->devicePixelRatioF()) - m_TexDisplay.xOffset) /
                           m_TexDisplay.scale));
  m_CurHoverPixel.setY(int((float(e->y() * ui->render->devicePixelRatioF()) - m_TexDisplay.yOffset) /
                           m_TexDisplay.scale));

  if(m_TexDisplay.resourceId != ResourceId())
  {
    TextureDescription *texptr = GetCurrentTexture();

    if(texptr != NULL)
    {
      if(e->buttons() & Qt::RightButton)
      {
        ui->render->setCursor(QCursor(Qt::CrossCursor));

        m_PickedPoint = m_CurHoverPixel;

        m_PickedPoint.setX(qBound(0, m_PickedPoint.x(), (int)texptr->width - 1));
        m_PickedPoint.setY(qBound(0, m_PickedPoint.y(), (int)texptr->height - 1));

        m_Ctx.Replay().AsyncInvoke(lit("PickPixelClick"),
                                   [this](IReplayController *r) { RT_PickPixelsAndUpdate(r); });
      }
      else if(e->buttons() == Qt::NoButton)
      {
        m_Ctx.Replay().AsyncInvoke(lit("PickPixelHover"),
                                   [this](IReplayController *r) { RT_PickHoverAndUpdate(r); });
      }
    }
  }

  QPoint curpos = QCursor::pos();

  if(e->buttons() & Qt::LeftButton)
  {
    if(qAbs(m_DragStartPos.x() - curpos.x()) > ui->renderHScroll->singleStep() ||
       qAbs(m_DragStartPos.y() - curpos.y()) > ui->renderVScroll->singleStep())
    {
      setScrollPosition(QPoint(m_DragStartScroll.x() + (curpos.x() - m_DragStartPos.x()),
                               m_DragStartScroll.y() + (curpos.y() - m_DragStartPos.y())));
    }

    ui->render->setCursor(QCursor(Qt::SizeAllCursor));
  }

  if(e->buttons() == Qt::NoButton)
  {
    ui->render->unsetCursor();
  }

  UI_UpdateStatusText();
}

void TextureViewer::render_mouseClick(QMouseEvent *e)
{
  ui->render->setFocus();

  if(e->buttons() & Qt::RightButton)
    render_mouseMove(e);

  if(e->buttons() & Qt::LeftButton)
  {
    m_DragStartPos = QCursor::pos();
    m_DragStartScroll = getScrollPosition();

    ui->render->setCursor(QCursor(Qt::SizeAllCursor));
  }
}

void TextureViewer::render_resize(QResizeEvent *e)
{
  UI_UpdateFittedScale();
  UI_CalcScrollbars();

  INVOKE_MEMFN(RT_UpdateAndDisplay);
}

void TextureViewer::render_keyPress(QKeyEvent *e)
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr == NULL)
    return;

  if(e->matches(QKeySequence::Copy))
  {
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(ui->texStatusName->text() + ui->texStatusDim->text() +
                       ui->texStatusFormat->text() + lit(" | ") + ui->hoverText->text() +
                       ui->pickedText->text());
  }

  if(!m_Ctx.IsCaptureLoaded())
    return;

  if((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_G)
  {
    ShowGotoPopup();
  }

  bool nudged = false;

  int increment = 1 << (int)m_TexDisplay.subresource.mip;

  if(e->key() == Qt::Key_Up && m_PickedPoint.y() > 0)
  {
    m_PickedPoint -= QPoint(0, increment);
    nudged = true;
  }
  else if(e->key() == Qt::Key_Down && m_PickedPoint.y() < (int)texptr->height - 1)
  {
    m_PickedPoint += QPoint(0, increment);
    nudged = true;
  }
  else if(e->key() == Qt::Key_Left && m_PickedPoint.x() > 0)
  {
    m_PickedPoint -= QPoint(increment, 0);
    nudged = true;
  }
  else if(e->key() == Qt::Key_Right && m_PickedPoint.x() < (int)texptr->width - 1)
  {
    m_PickedPoint += QPoint(increment, 0);
    nudged = true;
  }

  if(nudged)
  {
    m_PickedPoint = QPoint(qBound(0, m_PickedPoint.x(), (int)texptr->width - 1),
                           qBound(0, m_PickedPoint.y(), (int)texptr->height - 1));
    e->accept();

    m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
      RT_PickPixelsAndUpdate(r);
      RT_UpdateAndDisplay(r);
    });

    UI_UpdateStatusText();
  }
}

float TextureViewer::CurMaxScrollX()
{
  TextureDescription *texptr = GetCurrentTexture();

  QSizeF size(1.0f, 1.0f);

  if(texptr != NULL)
    size = QSizeF(texptr->width, texptr->height);

  return realRenderWidth() - size.width() * m_TexDisplay.scale;
}

float TextureViewer::CurMaxScrollY()
{
  TextureDescription *texptr = GetCurrentTexture();

  QSizeF size(1.0f, 1.0f);

  if(texptr != NULL)
    size = QSizeF(texptr->width, texptr->height);

  return realRenderHeight() - size.height() * m_TexDisplay.scale;
}

QPoint TextureViewer::getScrollPosition()
{
  return QPoint((int)m_TexDisplay.xOffset, m_TexDisplay.yOffset);
}

void TextureViewer::setScrollPosition(const QPoint &pos)
{
  m_TexDisplay.xOffset = qMax(CurMaxScrollX(), (float)pos.x());
  m_TexDisplay.yOffset = qMax(CurMaxScrollY(), (float)pos.y());

  m_TexDisplay.xOffset = qMin(0.0f, m_TexDisplay.xOffset);
  m_TexDisplay.yOffset = qMin(0.0f, m_TexDisplay.yOffset);

  if(ScrollUpdateScrollbars)
  {
    ScrollUpdateScrollbars = false;

    if(ui->renderHScroll->isEnabled())
      ui->renderHScroll->setValue(qBound(0, -int(m_TexDisplay.xOffset), ui->renderHScroll->maximum()));

    if(ui->renderVScroll->isEnabled())
      ui->renderVScroll->setValue(qBound(0, -int(m_TexDisplay.yOffset), ui->renderVScroll->maximum()));

    ScrollUpdateScrollbars = true;
  }

  INVOKE_MEMFN(RT_UpdateAndDisplay);
}

void TextureViewer::UI_CalcScrollbars()
{
  TextureDescription *texptr = GetCurrentTexture();

  QSizeF size(1.0f, 1.0f);

  if(texptr != NULL)
  {
    size = QSizeF(texptr->width, texptr->height);
  }

  if((int)floor(size.width() * m_TexDisplay.scale) <= realRenderWidth())
  {
    ui->renderHScroll->setEnabled(false);
  }
  else
  {
    ui->renderHScroll->setEnabled(true);

    ui->renderHScroll->setMaximum((int)ceil(size.width() * m_TexDisplay.scale - realRenderWidth()));
    ui->renderHScroll->setPageStep(qMax(1, ui->renderHScroll->maximum() / 6));
    ui->renderHScroll->setSingleStep(int(m_TexDisplay.scale));
  }

  if((int)floor(size.height() * m_TexDisplay.scale) <= realRenderHeight())
  {
    ui->renderVScroll->setEnabled(false);
  }
  else
  {
    ui->renderVScroll->setEnabled(true);

    ui->renderVScroll->setMaximum((int)ceil(size.height() * m_TexDisplay.scale - realRenderHeight()));
    ui->renderVScroll->setPageStep(qMax(1, ui->renderVScroll->maximum() / 6));
    ui->renderVScroll->setSingleStep(int(m_TexDisplay.scale));
  }
}

void TextureViewer::on_renderHScroll_valueChanged(int position)
{
  if(!ScrollUpdateScrollbars)
    return;

  ScrollUpdateScrollbars = false;

  {
    float delta = (float)position / (float)ui->renderHScroll->maximum();
    setScrollPosition(QPoint((int)(CurMaxScrollX() * delta), getScrollPosition().y()));
  }

  ScrollUpdateScrollbars = true;
}

void TextureViewer::on_renderVScroll_valueChanged(int position)
{
  if(!ScrollUpdateScrollbars)
    return;

  ScrollUpdateScrollbars = false;

  {
    float delta = (float)position / (float)ui->renderVScroll->maximum();
    setScrollPosition(QPoint(getScrollPosition().x(), (int)(CurMaxScrollY() * delta)));
  }

  ScrollUpdateScrollbars = true;
}

void TextureViewer::updateBackgroundColors()
{
  ui->render->SetBackCol(backCol);
  ui->pixelContext->SetBackCol(backCol);
}

void TextureViewer::OnCaptureLoaded()
{
  Reset();

  WindowingData renderData = ui->render->GetWidgetWindowingData();
  WindowingData contextData = ui->pixelContext->GetWidgetWindowingData();

  ui->saveTex->setEnabled(true);
  ui->locationGoto->setEnabled(true);
  ui->viewTexBuffer->setEnabled(true);

  ui->pixelHistory->setEnabled(false);
  ui->pixelHistory->setToolTip(QString());

  ui->debugPixelContext->setEnabled(false);
  ui->pixelHistory->setToolTip(QString());

  m_TexDisplay.backgroundColor =
      backCol.isValid() ? FloatVector(backCol.redF(), backCol.greenF(), backCol.blueF(), 1.0f)
                        : FloatVector();

  m_Ctx.Replay().BlockInvoke([renderData, contextData, this](IReplayController *r) {
    m_Output = r->CreateOutput(renderData, ReplayOutputType::Texture);

    m_Output->SetPixelContext(contextData);

    ui->render->SetOutput(m_Output);
    ui->pixelContext->SetOutput(m_Output);

    RT_UpdateAndDisplay(r);

    GUIInvoke::call(this, [this]() { OnEventChanged(m_Ctx.CurEvent()); });
  });

  m_Watcher = new QFileSystemWatcher({ConfigFilePath(QString())}, this);

  QObject::connect(m_Watcher, &QFileSystemWatcher::fileChanged, this,
                   &TextureViewer::customShaderModified);
  QObject::connect(m_Watcher, &QFileSystemWatcher::directoryChanged, this,
                   &TextureViewer::customShaderModified);

  m_NeedCustomReload = true;
}

void TextureViewer::Reset()
{
  m_CachedTexture = NULL;

  m_PickedPoint.setX(-1);
  m_PickedPoint.setY(-1);

  memset(&m_TexDisplay, 0, sizeof(m_TexDisplay));
  m_TexDisplay.backgroundColor =
      backCol.isValid() ? FloatVector(backCol.redF(), backCol.greenF(), backCol.blueF(), 1.0f)
                        : FloatVector();

  m_Output = NULL;

  m_TextureSettings.clear();

  m_PrevSize = QSizeF();
  m_HighWaterStatusLength = 0;

  ui->rangeHistogram->setRange(0.0f, 1.0f);

  ui->textureListFilter->setCurrentIndex(0);

  ui->renderHScroll->setEnabled(false);
  ui->renderVScroll->setEnabled(false);

  ui->pixelHistory->setEnabled(false);
  ui->pixelHistory->setToolTip(QString());
  ui->debugPixelContext->setEnabled(false);
  ui->debugPixelContext->setToolTip(QString());

  ui->texStatusName->setText(QString());
  ui->texStatusDim->setText(QString());
  ui->texStatusFormat->setText(QString());
  ui->hoverText->setText(QString());
  ui->pickedText->setText(QString());
  ui->renderContainer->setWindowTitle(tr("Current"));
  ui->mipLevel->clear();
  ui->sliceFace->clear();

  UI_SetScale(1.0f);

  ui->channels->setCurrentIndex(0);
  ui->overlay->setCurrentIndex(0);

  {
    QPalette Pal(palette());

    Pal.setColor(QPalette::Background, Qt::black);

    ui->pickSwatch->setAutoFillBackground(true);
    ui->pickSwatch->setPalette(Pal);
  }

  ui->customShader->clear();

  updateBackgroundColors();

  ui->inputThumbs->clearThumbs();
  ui->outputThumbs->clearThumbs();

  UI_UpdateTextureDetails();
  UI_UpdateChannels();
}

void TextureViewer::refreshTextureList()
{
  on_textureListFilter_currentIndexChanged(ui->textureListFilter->currentIndex());
}

void addToRoot(RDTreeWidgetItem *root, const TextureDescription &t)
{
  const QVariant &res = QVariant::fromValue(t.resourceId);
  RDTreeWidgetItem *child =
      new RDTreeWidgetItem({res, t.width, t.height, (3 == t.dimension ? t.depth : t.arraysize),
                            t.mips, t.format.Name(), QString()});

  child->setTag(res);
  root->addChild(child);
}

void TextureViewer::refreshTextureList(FilterType filterType, const QString &filterStr)
{
  ui->textureList->beginUpdate();
  ui->textureList->clearSelection();

  ui->textureList->clear();
  RDTreeWidgetItem *root = ui->textureList->invisibleRootItem();

  TextureCategory rtFlags = TextureCategory::ColorTarget | TextureCategory::DepthTarget;

  for(const TextureDescription &t : m_Ctx.GetTextures())
  {
    if(filterType == FilterType::Textures)
    {
      if(!(t.creationFlags & rtFlags))
        addToRoot(root, t);
    }
    else if(filterType == FilterType::RenderTargets)
    {
      if((t.creationFlags & rtFlags))
        addToRoot(root, t);
    }
    else
    {
      if(filterStr.isEmpty())
      {
        addToRoot(root, t);
      }
      else
      {
        if(QString(m_Ctx.GetResourceName(t.resourceId)).contains(filterStr, Qt::CaseInsensitive) ||
           QString::number(t.width).contains(filterStr, Qt::CaseInsensitive) ||
           QString::number(t.height).contains(filterStr, Qt::CaseInsensitive) ||
           QString::number((3 == t.dimension ? t.depth : t.arraysize))
               .contains(filterStr, Qt::CaseInsensitive) ||
           QString::number(t.mips).contains(filterStr, Qt::CaseInsensitive) ||
           QString(t.format.Name()).contains(filterStr, Qt::CaseInsensitive))
          addToRoot(root, t);
      }
    }
  }

  ui->textureList->setSelectedItem(root);

  ui->textureList->sortByColumn(ui->textureList->header()->sortIndicatorSection(),
                                ui->textureList->header()->sortIndicatorOrder());

  ui->textureList->setUpdatesEnabled(true);

  ui->textureList->endUpdate();
}

void TextureViewer::OnCaptureClosed()
{
  Reset();

  refreshTextureList();

  delete m_Watcher;
  m_Watcher = NULL;

  RemoveTextureTabs(0);

  m_LockedTabs.clear();

  ui->customShader->clear();
  m_CustomShaders.clear();

  ui->saveTex->setEnabled(false);
  ui->locationGoto->setEnabled(false);
  ui->viewTexBuffer->setEnabled(false);

  UI_UpdateChannels();
}

void TextureViewer::OnEventChanged(uint32_t eventId)
{
  bool copy = false, clear = false, compute = false;
  Following::GetActionContext(m_Ctx, copy, clear, compute);

  ShaderStage stages[] = {ShaderStage::Vertex, ShaderStage::Hull, ShaderStage::Domain,
                          ShaderStage::Geometry, ShaderStage::Pixel};

  int count = 5;

  if(compute)
  {
    stages[0] = ShaderStage::Compute;
    count = 1;
  }

  for(int i = 0; i < count; i++)
  {
    ShaderStage stage = stages[i];

    const ShaderBindpointMapping &mapping = Following::GetMapping(m_Ctx, stage);

    if(!mapping.readOnlyResources.empty())
      m_ReadOnlyResources[(uint32_t)stage] = Following::GetReadOnlyResources(m_Ctx, stage, true);
    else
      m_ReadOnlyResources[(uint32_t)stage].clear();

    if(!mapping.readWriteResources.empty())
      m_ReadWriteResources[(uint32_t)stage] = Following::GetReadWriteResources(m_Ctx, stage, true);
    else
      m_ReadWriteResources[(uint32_t)stage].clear();
  }

  UI_UpdateCachedTexture();

  TextureDescription *CurrentTexture = GetCurrentTexture();

  if(!currentTextureIsLocked() ||
     (CurrentTexture != NULL && m_TexDisplay.resourceId != CurrentTexture->resourceId))
    UI_OnTextureSelectionChanged(true);

  if(m_Output == NULL)
    return;

  UI_CreateThumbnails();

  UI_UpdateTextureDetails();

  if(m_ResourceCacheID != m_Ctx.ResourceNameCacheID())
  {
    m_ResourceCacheID = m_Ctx.ResourceNameCacheID();
    refreshTextureList();
  }

  // iterate over locked tabs, and update the name if it's changed
  for(QWidget *w : m_LockedTabs.values())
  {
    ResourceId id = w->property("id").value<ResourceId>();
    w->setWindowTitle(m_Ctx.GetResourceName(id));
  }

  rdcarray<BoundResource> RTs = Following::GetOutputTargets(m_Ctx);
  BoundResource Depth = Following::GetDepthTarget(m_Ctx);

  int outIndex = 0;
  int inIndex = 0;

  ui->outputThumbs->setUpdatesEnabled(false);
  ui->inputThumbs->setUpdatesEnabled(false);

  for(int rt = 0; rt < RTs.count(); rt++)
  {
    ResourcePreview *prev;

    if(outIndex < ui->outputThumbs->thumbs().size())
      prev = ui->outputThumbs->thumbs()[outIndex];
    else
      prev = UI_CreateThumbnail(ui->outputThumbs);

    outIndex++;

    Following follow(*this, FollowType::OutputColor, ShaderStage::Pixel, rt, 0);
    QString bindName = (copy || clear) ? tr("Destination") : QString();
    QString slotName = (copy || clear)
                           ? tr("DST")
                           : QString(m_Ctx.CurPipelineState().OutputAbbrev() + QString::number(rt));

    InitResourcePreview(prev, RTs[rt], false, follow, bindName, slotName);
  }

  // depth
  {
    ResourcePreview *prev;

    if(outIndex < ui->outputThumbs->thumbs().size())
      prev = ui->outputThumbs->thumbs()[outIndex];
    else
      prev = UI_CreateThumbnail(ui->outputThumbs);

    outIndex++;

    Following follow(*this, FollowType::OutputDepth, ShaderStage::Pixel, 0, 0);

    InitResourcePreview(prev, Depth, false, follow, QString(), tr("DS"));
  }

  const rdcarray<ShaderResource> empty;

  // display resources used for all stages
  for(int i = 0; i < count; i++)
  {
    ShaderStage stage = stages[i];

    const ShaderReflection *details = Following::GetReflection(m_Ctx, stage);
    const ShaderBindpointMapping &mapping = Following::GetMapping(m_Ctx, stage);

    InitStageResourcePreviews(stage, details != NULL ? details->readWriteResources : empty,
                              mapping.readWriteResources, m_ReadWriteResources[(uint32_t)stage],
                              ui->outputThumbs, outIndex, copy, true);

    InitStageResourcePreviews(stage, details != NULL ? details->readOnlyResources : empty,
                              mapping.readOnlyResources, m_ReadOnlyResources[(uint32_t)stage],
                              ui->inputThumbs, inIndex, copy, false);
  }

  // hide others
  const QVector<ResourcePreview *> &outThumbs = ui->outputThumbs->thumbs();

  for(; outIndex < outThumbs.size(); outIndex++)
  {
    ResourcePreview *prev = outThumbs[outIndex];
    prev->setResourceName(QString());
    prev->setActive(false);
    prev->setSelected(false);
  }

  ui->outputThumbs->refreshLayout();

  const QVector<ResourcePreview *> &inThumbs = ui->inputThumbs->thumbs();

  for(; inIndex < inThumbs.size(); inIndex++)
  {
    ResourcePreview *prev = inThumbs[inIndex];
    prev->setResourceName(QString());
    prev->setActive(false);
    prev->setSelected(false);
  }

  ui->inputThumbs->refreshLayout();

  INVOKE_MEMFN(RT_UpdateAndDisplay);

  if(ui->autoFit->isChecked())
    AutoFitRange();
}

QVariant TextureViewer::persistData()
{
  QVariantMap state = ui->dockarea->saveState();

  QVariantList columns;
  for(int i = 0; i < TextureListFilter::Column_Count; i++)
  {
    QVariantMap col;

    bool hidden = ui->textureList->header()->isSectionHidden(i);

    col[lit("hidden")] = hidden;
    columns.push_back(col);
  }

  state[lit("columns")] = columns;
  state[lit("backCol")] = backCol;
  state[lit("checker")] = !backCol.isValid();

  return state;
}

void TextureViewer::setPersistData(const QVariant &persistData)
{
  QVariantMap state = persistData.toMap();

  QVariantList columns = state[lit("columns")].toList();
  for(int i = 0; i < columns.count() && i < TextureListFilter::Column_Count; i++)
  {
    QVariantMap col = columns[i].toMap();

    bool hidden = col[lit("hidden")].toBool();

    if(hidden)
      ui->textureList->header()->hideSection(i);
    else
      ui->textureList->header()->showSection(i);
  }

  backCol = state[lit("backCol")].value<QColor>();
  bool checker = state[lit("checker")].value<bool>();

  if(checker)
    backCol = QColor();

  ui->backcolorPick->setChecked(!checker);
  ui->checkerBack->setChecked(checker);

  m_TexDisplay.backgroundColor =
      checker ? FloatVector() : FloatVector(backCol.redF(), backCol.greenF(), backCol.blueF(), 1.0f);

  RemoveTextureTabs(0);

  m_LockedTabs.clear();

  updateBackgroundColors();

  ui->dockarea->restoreState(state);

  SetupTextureTabs();
}

float TextureViewer::GetFitScale()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr == NULL)
    return 1.0f;

  float xscale = (float)realRenderWidth() / (float)texptr->width;
  float yscale = (float)realRenderHeight() / (float)texptr->height;

  return qMin(xscale, yscale);
}

int TextureViewer::realRenderWidth() const
{
  return ui->render->width() * ui->render->devicePixelRatioF();
}

int TextureViewer::realRenderHeight() const
{
  return ui->render->height() * ui->render->devicePixelRatioF();
}

void TextureViewer::UI_UpdateFittedScale()
{
  if(ui->fitToWindow->isChecked())
    UI_SetScale(1.0f);
}

void TextureViewer::UI_SetScale(float s)
{
  UI_SetScale(s, ui->render->width() / 2, ui->render->height() / 2);
}

void TextureViewer::UI_SetScale(float s, int x, int y)
{
  if(ui->fitToWindow->isChecked())
    s = GetFitScale();

  float prevScale = m_TexDisplay.scale;

  m_TexDisplay.scale = qBound(0.1f, s, 256.0f);

  INVOKE_MEMFN(RT_UpdateAndDisplay);

  float scaleDelta = (m_TexDisplay.scale / prevScale);

  QPoint newPos = getScrollPosition();

  newPos -= QPoint(x, y);
  newPos = QPoint((int)(newPos.x() * scaleDelta), (int)(newPos.y() * scaleDelta));
  newPos += QPoint(x, y);

  setCurrentZoomValue(m_TexDisplay.scale);

  UI_CalcScrollbars();

  setScrollPosition(newPos);
}

void TextureViewer::setCurrentZoomValue(float zoom)
{
  ui->zoomOption->setCurrentText(QString::number(ceil(zoom * 100)) + lit("%"));
}

void TextureViewer::setFitToWindow(bool checked)
{
  if(checked)
  {
    UI_UpdateFittedScale();
    ui->fitToWindow->setChecked(true);
  }
  else if(!checked)
  {
    ui->fitToWindow->setChecked(false);
    float curScale = m_TexDisplay.scale;
    ui->zoomOption->setCurrentText(QString());
    setCurrentZoomValue(curScale);
  }
}

void TextureViewer::on_fitToWindow_toggled(bool checked)
{
  UI_UpdateFittedScale();
}

void TextureViewer::on_zoomExactSize_clicked()
{
  ui->fitToWindow->setChecked(false);
  UI_SetScale(1.0f);
}

void TextureViewer::on_zoomOption_currentIndexChanged(int index)
{
  if(index >= 0)
  {
    setFitToWindow(false);
    ui->zoomOption->setCurrentText(ui->zoomOption->itemText(index));
    UI_SetScale(GetZoomLevel());
  }
}

void TextureViewer::zoomOption_returnPressed()
{
  UI_SetScale(GetZoomLevel());
}

void TextureViewer::on_overlay_currentIndexChanged(int index)
{
  m_TexDisplay.overlay = DebugOverlay::NoOverlay;

  if(ui->overlay->currentIndex() > 0)
    m_TexDisplay.overlay = (DebugOverlay)ui->overlay->currentIndex();

#define ANALYTICS_OVERLAY(name) \
  case DebugOverlay::name: ANALYTIC_SET(TextureOverlays.name, true); break;

  switch(m_TexDisplay.overlay)
  {
    ANALYTICS_OVERLAY(Drawcall);
    ANALYTICS_OVERLAY(Wireframe);
    ANALYTICS_OVERLAY(Depth);
    ANALYTICS_OVERLAY(Stencil);
    ANALYTICS_OVERLAY(BackfaceCull);
    ANALYTICS_OVERLAY(ViewportScissor);
    ANALYTICS_OVERLAY(NaN);
    ANALYTICS_OVERLAY(Clipping);
    ANALYTICS_OVERLAY(ClearBeforePass);
    ANALYTICS_OVERLAY(ClearBeforeDraw);
    ANALYTICS_OVERLAY(QuadOverdrawPass);
    ANALYTICS_OVERLAY(QuadOverdrawDraw);
    ANALYTICS_OVERLAY(TriangleSizePass);
    ANALYTICS_OVERLAY(TriangleSizeDraw);
    default: break;
  }

#undef ANALYTICS_OVERLAY

  INVOKE_MEMFN(RT_UpdateAndDisplay);
  if(m_Output != NULL && m_PickedPoint.x() >= 0 && m_PickedPoint.y() >= 0)
  {
    INVOKE_MEMFN(RT_PickPixelsAndUpdate);
  }
}

void TextureViewer::channelsWidget_mouseClicked(QMouseEvent *event)
{
  RDToolButton *s = qobject_cast<RDToolButton *>(QObject::sender());

  if(event->button() == Qt::RightButton && s)
  {
    bool checkd = false;

    RDToolButton *butts[] = {ui->channelRed, ui->channelGreen, ui->channelBlue, ui->channelAlpha};

    for(RDToolButton *b : butts)
    {
      if(b->isChecked() && b != s)
        checkd = true;
      if(!b->isChecked() && b == s)
        checkd = true;
    }

    ui->channelRed->setChecked(!checkd);
    ui->channelGreen->setChecked(!checkd);
    ui->channelBlue->setChecked(!checkd);
    ui->channelAlpha->setChecked(!checkd);
    s->setChecked(checkd);
  }
}

void TextureViewer::range_rangeUpdated()
{
  m_TexDisplay.rangeMin = ui->rangeHistogram->blackPoint();
  m_TexDisplay.rangeMax = ui->rangeHistogram->whitePoint();

  ui->rangeBlack->setText(Formatter::Format(m_TexDisplay.rangeMin));
  ui->rangeWhite->setText(Formatter::Format(m_TexDisplay.rangeMax));

  if(m_NoRangePaint)
    return;

  INVOKE_MEMFN(RT_UpdateAndDisplay);

  if(m_Output == NULL)
  {
    ui->render->update();
    ui->pixelContext->update();
  }
}

void TextureViewer::rangePoint_textChanged(QString text)
{
  m_RangePoint_Dirty = true;
}

void TextureViewer::rangePoint_Update()
{
  float black = ui->rangeHistogram->blackPoint();
  float white = ui->rangeHistogram->whitePoint();

  bool ok = false;
  double d = ui->rangeBlack->text().toDouble(&ok);

  if(ok)
    black = d;

  d = ui->rangeWhite->text().toDouble(&ok);

  if(ok)
    white = d;

  ui->rangeHistogram->setRange(black, white);

  INVOKE_MEMFN(RT_UpdateVisualRange);
}

void TextureViewer::rangePoint_leave()
{
  if(!m_RangePoint_Dirty)
    return;

  rangePoint_Update();

  m_RangePoint_Dirty = false;
}

void TextureViewer::rangePoint_keyPress(QKeyEvent *e)
{
  // escape key
  if(e->key() == Qt::Key_Escape)
  {
    m_RangePoint_Dirty = false;
    ui->rangeHistogram->setRange(ui->rangeHistogram->blackPoint(), ui->rangeHistogram->whitePoint());
  }
  if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)
  {
    rangePoint_Update();
  }
}

void TextureViewer::on_zoomRange_clicked()
{
  float black = ui->rangeHistogram->blackPoint();
  float white = ui->rangeHistogram->whitePoint();

  ui->autoFit->setChecked(false);

  ui->rangeHistogram->setRange(black, white);

  INVOKE_MEMFN(RT_UpdateVisualRange);
}

void TextureViewer::on_autoFit_clicked()
{
  AutoFitRange();
  ui->autoFit->setChecked(false);
}

void TextureViewer::on_autoFit_mouseClicked(QMouseEvent *e)
{
  if(e->buttons() & Qt::RightButton)
    ui->autoFit->setChecked(!ui->autoFit->isChecked());
}

void TextureViewer::on_reset01_clicked()
{
  UI_SetHistogramRange(GetCurrentTexture(), m_TexDisplay.typeCast);

  ui->autoFit->setChecked(false);

  INVOKE_MEMFN(RT_UpdateVisualRange);
}

void TextureViewer::on_visualiseRange_clicked()
{
  if(ui->visualiseRange->isChecked())
  {
    ui->rangeHistogram->setMinimumSize(QSize(300, 90));

    m_Visualise = true;
    INVOKE_MEMFN(RT_UpdateVisualRange);
  }
  else
  {
    m_Visualise = false;
    ui->rangeHistogram->setMinimumSize(QSize(200, 0));

    ui->rangeHistogram->setHistogramData({});
  }
}

void TextureViewer::AutoFitRange()
{
  // no capture loaded or buffer/empty texture currently being viewed - don't autofit
  if(!m_Ctx.IsCaptureLoaded() || GetCurrentTexture() == NULL || m_Output == NULL)
    return;

  m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {

    ResourceId textureId = m_TexDisplay.resourceId;
    Subresource sub = m_TexDisplay.subresource;
    CompType typeCast = m_TexDisplay.typeCast;

    if(m_TexDisplay.customShaderId != ResourceId() && m_Output->GetCustomShaderTexID() != ResourceId())
    {
      textureId = m_Output->GetCustomShaderTexID();
      sub.slice = sub.sample = 0;
      typeCast = CompType::Typeless;
    }

    PixelValue min, max;
    rdctie(min, max) = r->GetMinMax(textureId, sub, typeCast);

    {
      float minval = FLT_MAX;
      float maxval = -FLT_MAX;

      bool changeRange = false;

      ResourceFormat fmt = GetCurrentTexture()->format;

      if(m_TexDisplay.customShaderId != ResourceId())
      {
        fmt.compType = CompType::Float;
      }
      if(fmt.compType == CompType::Typeless && m_TexDisplay.typeCast == CompType::UInt)
        fmt.compType = CompType::UInt;
      if(fmt.compType == CompType::Typeless && m_TexDisplay.typeCast == CompType::SInt)
        fmt.compType = CompType::SInt;

      for(int i = 0; i < 4; i++)
      {
        if(fmt.compType == CompType::UInt)
        {
          min.floatValue[i] = min.uintValue[i];
          max.floatValue[i] = max.uintValue[i];
        }
        else if(fmt.compType == CompType::SInt)
        {
          min.floatValue[i] = min.intValue[i];
          max.floatValue[i] = max.intValue[i];
        }
      }

      if(m_TexDisplay.red)
      {
        minval = qMin(minval, min.floatValue[0]);
        maxval = qMax(maxval, max.floatValue[0]);
        changeRange = true;
      }
      if(m_TexDisplay.green && fmt.compCount > 1)
      {
        minval = qMin(minval, min.floatValue[1]);
        maxval = qMax(maxval, max.floatValue[1]);
        changeRange = true;
      }
      if(m_TexDisplay.blue && fmt.compCount > 2)
      {
        minval = qMin(minval, min.floatValue[2]);
        maxval = qMax(maxval, max.floatValue[2]);
        changeRange = true;
      }
      if(m_TexDisplay.alpha && fmt.compCount > 3)
      {
        minval = qMin(minval, min.floatValue[3]);
        maxval = qMax(maxval, max.floatValue[3]);
        changeRange = true;
      }

      if(changeRange)
      {
        GUIInvoke::call(this, [this, minval, maxval]() {
          ui->rangeHistogram->setRange(minval, maxval);
          INVOKE_MEMFN(RT_UpdateVisualRange);
        });
      }
    }
  });
}

void TextureViewer::on_backcolorPick_clicked()
{
  QColor col = QColorDialog::getColor(Qt::black, this, tr("Choose background colour"));

  if(!col.isValid())
  {
    ui->backcolorPick->setChecked(!ui->checkerBack->isChecked());
    return;
  }

  col = col.toRgb();
  m_TexDisplay.backgroundColor = FloatVector(col.redF(), col.greenF(), col.blueF(), 1.0f);

  backCol = col;

  updateBackgroundColors();

  ui->backcolorPick->setChecked(true);
  ui->checkerBack->setChecked(false);

  INVOKE_MEMFN(RT_UpdateAndDisplay);

  if(m_Output == NULL)
  {
    ui->render->update();
    ui->pixelContext->update();
  }
}

void TextureViewer::on_checkerBack_clicked()
{
  ui->checkerBack->setChecked(true);
  ui->backcolorPick->setChecked(false);

  backCol = QColor();

  m_TexDisplay.backgroundColor = FloatVector();

  updateBackgroundColors();

  INVOKE_MEMFN(RT_UpdateAndDisplay);

  if(m_Output == NULL)
  {
    ui->render->update();
    ui->pixelContext->update();
  }
}

void TextureViewer::on_mipLevel_currentIndexChanged(int index)
{
  TextureDescription *texptr = GetCurrentTexture();
  if(texptr == NULL)
    return;

  TextureDescription &tex = *texptr;

  uint32_t prevSlice = m_TexDisplay.subresource.slice;

  if(tex.mips > 1)
  {
    m_TexDisplay.subresource.mip = (uint32_t)qMax(0, index);
    m_TexDisplay.subresource.sample = 0;
  }
  else
  {
    m_TexDisplay.subresource.mip = 0;
    m_TexDisplay.subresource.sample = (uint32_t)qMax(0, index);
    if(m_TexDisplay.subresource.sample == tex.msSamp)
      m_TexDisplay.subresource.sample = ~0U;
  }

  // For 3D textures, update the slice list for this mip
  if(tex.depth > 1)
  {
    uint32_t newSlice = prevSlice >> (int)m_TexDisplay.subresource.mip;

    uint32_t numSlices = qMax(1U, tex.depth >> (int)m_TexDisplay.subresource.mip);

    ui->sliceFace->clear();

    for(uint32_t i = 0; i < numSlices; i++)
      ui->sliceFace->addItem(tr("Slice %1").arg(i));

    // changing sliceFace index will handle updating range & re-picking
    ui->sliceFace->setCurrentIndex((int)qBound(0U, newSlice, numSlices - 1));

    return;
  }

  INVOKE_MEMFN(RT_UpdateVisualRange);

  if(m_Output != NULL && m_PickedPoint.x() >= 0 && m_PickedPoint.y() >= 0)
  {
    INVOKE_MEMFN(RT_PickPixelsAndUpdate);
  }

  INVOKE_MEMFN(RT_UpdateAndDisplay);
}

void TextureViewer::on_sliceFace_currentIndexChanged(int index)
{
  TextureDescription *texptr = GetCurrentTexture();
  if(texptr == NULL)
    return;

  TextureDescription &tex = *texptr;
  m_TexDisplay.subresource.slice = (uint32_t)qMax(0, index);

  INVOKE_MEMFN(RT_UpdateVisualRange);

  if(m_Output != NULL && m_PickedPoint.x() >= 0 && m_PickedPoint.y() >= 0)
  {
    INVOKE_MEMFN(RT_PickPixelsAndUpdate);
  }

  INVOKE_MEMFN(RT_UpdateAndDisplay);
}

void TextureViewer::on_locationGoto_clicked()
{
  ShowGotoPopup();
}

void TextureViewer::ShowGotoPopup()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr)
  {
    QPoint p = m_PickedPoint;

    p.setX(p.x() >> (int)m_TexDisplay.subresource.mip);
    p.setY(p.y() >> (int)m_TexDisplay.subresource.mip);

    uint32_t mipHeight = qMax(1U, texptr->height >> (int)m_TexDisplay.subresource.mip);

    if(ShouldFlipForGL())
      p.setY((int)(mipHeight - 1) - p.y());
    if(m_TexDisplay.flipY)
      p.setY((int)(mipHeight - 1) - p.y());

    m_Goto->show(ui->render, p);
  }
}

bool TextureViewer::ShouldFlipForGL()
{
  if(m_Ctx.APIProps().pipelineType == GraphicsAPI::OpenGL)
  {
    // lower left is the default clip origin, which needs the Y flip
    return m_Ctx.CurGLPipelineState()->vertexProcessing.clipOriginLowerLeft;
  }

  return false;
}

void TextureViewer::on_viewTexBuffer_clicked()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr)
  {
    IBufferViewer *viewer =
        m_Ctx.ViewTextureAsBuffer(texptr->resourceId, m_TexDisplay.subresource,
                                  BufferFormatter::GetTextureFormatString(*texptr));

    viewer->ScrollToRow(m_PickedPoint.y());
    viewer->ScrollToColumn(m_PickedPoint.x() * texptr->format.compCount + 1);

    m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
  }
}

void TextureViewer::on_resourceDetails_clicked()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(texptr)
  {
    if(!m_Ctx.HasResourceInspector())
      m_Ctx.ShowResourceInspector();

    m_Ctx.GetResourceInspector()->Inspect(texptr->resourceId);

    ToolWindowManager::raiseToolWindow(m_Ctx.GetResourceInspector()->Widget());
  }
}

void TextureViewer::on_saveTex_clicked()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(!texptr || !m_Output)
    return;

  // overwrite save params with current texture display settings
  m_SaveConfig.resourceId = m_TexDisplay.resourceId;
  m_SaveConfig.typeCast = m_TexDisplay.typeCast;
  m_SaveConfig.slice.sliceIndex = (int)m_TexDisplay.subresource.slice;
  m_SaveConfig.mip = (int)m_TexDisplay.subresource.mip;

  m_SaveConfig.channelExtract = -1;
  if(m_TexDisplay.red && !m_TexDisplay.green && !m_TexDisplay.blue && !m_TexDisplay.alpha)
    m_SaveConfig.channelExtract = 0;
  if(!m_TexDisplay.red && m_TexDisplay.green && !m_TexDisplay.blue && !m_TexDisplay.alpha)
    m_SaveConfig.channelExtract = 1;
  if(!m_TexDisplay.red && !m_TexDisplay.green && m_TexDisplay.blue && !m_TexDisplay.alpha)
    m_SaveConfig.channelExtract = 2;
  if(!m_TexDisplay.red && !m_TexDisplay.green && !m_TexDisplay.blue && m_TexDisplay.alpha)
    m_SaveConfig.channelExtract = 3;

  m_SaveConfig.comp.blackPoint = m_TexDisplay.rangeMin;
  m_SaveConfig.comp.whitePoint = m_TexDisplay.rangeMax;
  m_SaveConfig.alphaCol = m_TexDisplay.backgroundColor;

  if(m_TexDisplay.customShaderId != ResourceId())
  {
    ResourceId id;
    m_Ctx.Replay().BlockInvoke(
        [this, &id](IReplayController *r) { id = m_Output->GetCustomShaderTexID(); });

    if(id != ResourceId())
    {
      m_SaveConfig.resourceId = id;
      m_SaveConfig.typeCast = CompType::Typeless;
    }
  }

  ResourceId overlayTexID;
  if(m_TexDisplay.overlay != DebugOverlay::NoOverlay)
  {
    m_Ctx.Replay().BlockInvoke([this, &overlayTexID](IReplayController *r) {
      overlayTexID = m_Output->GetDebugOverlayTexID();
    });
  }
  const bool hasSelectedOverlay = (m_TexDisplay.overlay != DebugOverlay::NoOverlay);
  const bool hasOverlay = (hasSelectedOverlay && overlayTexID != ResourceId());
  TextureSaveDialog saveDialog(*texptr, hasOverlay, m_SaveConfig, this);
  int res = RDDialog::show(&saveDialog);

  m_SaveConfig = saveDialog.config();

  if(saveDialog.saveOverlayInstead())
  {
    m_SaveConfig.resourceId = overlayTexID;
    m_SaveConfig.typeCast = CompType::Typeless;

    if(m_TexDisplay.overlay == DebugOverlay::QuadOverdrawDraw ||
       m_TexDisplay.overlay == DebugOverlay::QuadOverdrawPass ||
       m_TexDisplay.overlay == DebugOverlay::TriangleSizeDraw ||
       m_TexDisplay.overlay == DebugOverlay::TriangleSizePass)
    {
      m_SaveConfig.comp.blackPoint = 0.0f;
      m_SaveConfig.comp.whitePoint = 255.0f;
    }
  }

  if(res)
  {
    ANALYTIC_SET(Export.Texture, true);

    bool ret = false;
    QString fn = saveDialog.filename();

    m_Ctx.Replay().BlockInvoke(
        [this, &ret, fn](IReplayController *r) { ret = r->SaveTexture(m_SaveConfig, fn); });

    if(!ret)
    {
      RDDialog::critical(
          NULL, tr("Error saving texture"),
          tr("Error saving texture %1.\n\nCheck diagnostic log in Help menu for more details.").arg(fn));
    }
  }
}

void TextureViewer::on_debugPixelContext_clicked()
{
  if(m_PickedPoint.x() < 0 || m_PickedPoint.y() < 0)
    return;

  int x = m_PickedPoint.x() >> (int)m_TexDisplay.subresource.mip;
  int y = m_PickedPoint.y() >> (int)m_TexDisplay.subresource.mip;

  TextureDescription *texptr = GetCurrentTexture();

  uint32_t mipHeight = qMax(1U, texptr->height >> (int)m_TexDisplay.subresource.mip);

  if(m_TexDisplay.flipY)
    y = (int)(mipHeight - 1) - y;

  bool done = false;
  ShaderDebugTrace *trace = NULL;

  m_Ctx.Replay().AsyncInvoke([this, &trace, &done, x, y](IReplayController *r) {
    trace = r->DebugPixel((uint32_t)x, (uint32_t)y, m_TexDisplay.subresource.sample, ~0U);

    if(trace->debugger == NULL)
    {
      r->FreeTrace(trace);
      trace = NULL;
    }

    done = true;

  });

  QString debugContext = tr("Pixel %1,%2").arg(x).arg(y);

  // wait a short while before displaying the progress dialog (which won't show if we're already
  // done by the time we reach it)
  for(int i = 0; !done && i < 100; i++)
    QThread::msleep(5);

  ShowProgressDialog(this, tr("Debugging %1").arg(debugContext), [&done]() { return done; });

  // if we couldn't debug the pixel on this event, open up a pixel history
  if(!trace)
  {
    if(m_Ctx.APIProps().pixelHistory)
      on_pixelHistory_clicked();
    else
      RDDialog::critical(this, tr("Debug Error"), tr("Error debugging pixel."));
    return;
  }

  const ShaderReflection *shaderDetails =
      m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Pixel);
  const ShaderBindpointMapping &bindMapping =
      m_Ctx.CurPipelineState().GetBindpointMapping(ShaderStage::Pixel);
  ResourceId pipeline = m_Ctx.CurPipelineState().GetGraphicsPipelineObject();

  // viewer takes ownership of the trace
  IShaderViewer *s = m_Ctx.DebugShader(&bindMapping, shaderDetails, pipeline, trace, debugContext);

  m_Ctx.AddDockWindow(s->Widget(), DockReference::AddTo, this);
}

void TextureViewer::on_pixelHistory_clicked()
{
  TextureDescription *texptr = GetCurrentTexture();

  if(!texptr || !m_Output)
    return;

  ANALYTIC_SET(UIFeatures.PixelHistory, true);

  int x = m_PickedPoint.x() >> (int)m_TexDisplay.subresource.mip;
  int y = m_PickedPoint.y() >> (int)m_TexDisplay.subresource.mip;

  uint32_t mipHeight = qMax(1U, texptr->height >> (int)m_TexDisplay.subresource.mip);

  if(m_TexDisplay.flipY)
    y = (int)(mipHeight - 1) - y;

  IPixelHistoryView *hist = m_Ctx.ViewPixelHistory(texptr->resourceId, x, y, m_TexDisplay);

  m_Ctx.AddDockWindow(hist->Widget(), DockReference::TransientPopupArea, this, 0.3f);

  // we use this pointer to ensure that the history viewer is still visible (and hasn't been closed)
  // by the time we want to set the results.
  QPointer<QWidget> histWidget = hist->Widget();

  // add a short delay so that controls repainting after a new panel appears can get at the
  // render thread before we insert the long blocking pixel history task
  LambdaThread *thread = new LambdaThread([this, texptr, x, y, hist, histWidget]() {
    QThread::msleep(150);
    m_Ctx.Replay().AsyncInvoke([this, texptr, x, y, hist, histWidget](IReplayController *r) {
      rdcarray<PixelModification> history =
          r->PixelHistory(texptr->resourceId, (uint32_t)x, (int32_t)y, m_TexDisplay.subresource,
                          m_TexDisplay.typeCast);

      GUIInvoke::call(this, [hist, histWidget, history] {
        if(histWidget)
          hist->SetHistory(history);
      });
    });
  });
  thread->selfDelete(true);
  thread->start();
}

void TextureViewer::on_texListShow_clicked()
{
  if(ui->textureListFrame->isVisible())
  {
    ui->dockarea->moveToolWindow(ui->textureListFrame, ToolWindowManager::NoArea);
  }
  else
  {
    ui->textureListFilter->setCurrentText(QString());
    ui->dockarea->moveToolWindow(
        ui->textureListFrame,
        ToolWindowManager::AreaReference(ToolWindowManager::BottomOf,
                                         ui->dockarea->areaOf(ui->renderContainer), 0.2f));
    ui->dockarea->setToolWindowProperties(ui->textureListFrame, ToolWindowManager::HideOnClose);
  }
}

void TextureViewer::on_cancelTextureListFilter_clicked()
{
  ui->textureListFilter->setCurrentText(QString());
}

void TextureViewer::on_colSelect_clicked()
{
  QStringList headers;
  for(int i = 0; i < TextureListFilter::Column_Count; ++i)
  {
    headers.push_back(
        ui->textureList->model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
  }

  UpdateVisibleColumns(tr("Select Texture List Columns"), TextureListFilter::Column_Count,
                       ui->textureList->header(), headers);
}

void TextureViewer::on_textureListFilter_editTextChanged(const QString &text)
{
  refreshTextureList(FilterType::String, text);
}

void TextureViewer::on_textureListFilter_currentIndexChanged(int index)
{
  if(ui->textureListFilter->currentIndex() == 1)
    refreshTextureList(FilterType::Textures, QString());
  else if(ui->textureListFilter->currentIndex() == 2)
    refreshTextureList(FilterType::RenderTargets, QString());
  else
    refreshTextureList(FilterType::String, ui->textureListFilter->currentText());
}

void TextureViewer::texture_itemActivated(RDTreeWidgetItem *item, int column)
{
  QVariant tag = item->tag();
  if(!tag.canConvert<ResourceId>())
    return;

  TextureDescription *tex = m_Ctx.GetTexture(tag.value<ResourceId>());
  if(!tex)
    return;

  if(tex->type == TextureType::Buffer)
  {
    IBufferViewer *viewer = m_Ctx.ViewTextureAsBuffer(
        tex->resourceId, Subresource(), BufferFormatter::GetTextureFormatString(*tex));

    m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
  }
  else
  {
    CompType typeCast = CompType::Typeless;
    if(m_TextureSettings.contains(tex->resourceId))
      typeCast = m_TextureSettings[tex->resourceId].typeCast;
    ViewTexture(tex->resourceId, typeCast, true);
  }
}

bool TextureViewer::canCompileCustomShader(ShaderEncoding encoding)
{
  rdcarray<ShaderEncoding> supported = m_Ctx.CustomShaderEncodings();

  // if it's directly supported, we can trivially compile it
  if(supported.contains(encoding))
    return true;

  // otherwise see if we have a tool that can compile it for us

  for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors)
  {
    // if this tool transforms from the encoding to one we support, we can compile a shader of this
    // encoding
    if(tool.input == encoding && supported.contains(tool.output))
      return true;
  }

  // all options exhausted, we can't compile this
  return false;
}

void TextureViewer::reloadCustomShaders(const QString &filter)
{
  if(!m_Ctx.IsCaptureLoaded())
    return;

  if(filter.isEmpty())
  {
    QString prevtext = ui->customShader->currentText();

    QList<ResourceId> shaders = m_CustomShaders.values();

    m_Ctx.Replay().AsyncInvoke([shaders](IReplayController *r) {
      for(ResourceId s : shaders)
        r->FreeCustomShader(s);
    });

    ui->customShader->clear();
    m_CustomShaders.clear();

    ui->customShader->setCurrentText(prevtext);
  }
  else
  {
    QString fn = QFileInfo(filter).fileName();
    QString key = fn.toUpper();

    if(m_CustomShaders.contains(key))
    {
      if(m_CustomShadersBusy.contains(key))
        return;

      ResourceId freed = m_CustomShaders[key];
      m_Ctx.Replay().AsyncInvoke([freed](IReplayController *r) { r->FreeCustomShader(freed); });

      m_CustomShaders.remove(key);

      QString text = ui->customShader->currentText();

      for(int i = 0; i < ui->customShader->count(); i++)
      {
        if(ui->customShader->itemText(i).compare(fn, Qt::CaseInsensitive) == 0)
        {
          ui->customShader->removeItem(i);
          break;
        }
      }

      ui->customShader->setCurrentText(text);
    }
  }

  QStringList filters;
  for(auto it = encodingExtensions.begin(); it != encodingExtensions.end(); ++it)
  {
    if(!canCompileCustomShader(it.value()))
      continue;

    filters.push_back(lit("*.") + it.key());
  }

  QStringList files;
  QList<QDir> shaderDirectories = getShaderDirectories();
  for(const QDir &dir : shaderDirectories)
  {
    QStringList currentDirFiles =
        dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::IgnoreCase);
    for(const QString &f : currentDirFiles)
    {
      files.append(QDir::cleanPath(dir.absoluteFilePath(f)));
    }
  }

  QStringList watchedFiles = m_Watcher->files();
  if(!watchedFiles.isEmpty())
    m_Watcher->removePaths(watchedFiles);

  for(const QString &f : files)
  {
    QFileInfo fileInfo(f);
    QString fn = fileInfo.fileName();
    QString key = fn.toUpper();
    ShaderEncoding encoding = encodingExtensions[fileInfo.completeSuffix()];

    if(!filter.isEmpty() && filter.toUpper() != key)
      continue;

    m_Watcher->addPath(f);

    if(!m_CustomShaders.contains(key) && !m_CustomShadersBusy.contains(key))
    {
      QFile fileHandle(f);
      if(fileHandle.open(QFile::ReadOnly | QFile::Text))
      {
        QTextStream stream(&fileHandle);
        QString source = stream.readAll();

        bytebuf shaderBytes(source.toUtf8());

        rdcarray<ShaderEncoding> supported = m_Ctx.CustomShaderEncodings();
        rdcarray<ShaderSourcePrefix> prefixes = m_Ctx.CustomShaderSourcePrefixes();

        rdcstr errors;

        // we don't accept this encoding directly, need to compile
        if(!supported.contains(encoding))
        {
          for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors)
          {
            // pick the first tool that can convert to an accepted format
            if(tool.input == encoding && supported.contains(tool.output))
            {
              // apply any prefix needed
              for(const ShaderSourcePrefix &prefix : prefixes)
              {
                if(prefix.encoding == encoding)
                {
                  source = QString(prefix.prefix) + source;
                  break;
                }
              }

              ShaderToolOutput out =
                  tool.CompileShader(this, source, "main", ShaderStage::Pixel, "");

              errors = out.log;

              if(m_CustomShaderEditor.contains(key))
                m_CustomShaderEditor[key]->ShowErrors(errors);

              if(out.result.isEmpty())
                break;

              encoding = tool.output;
              shaderBytes = out.result;
              break;
            }
          }
        }

        // if the encoding still isn't supported, all tools failed. Bail out now
        if(!supported.contains(encoding))
        {
          if(m_CustomShaderEditor.contains(key))
            m_CustomShaderEditor[key]->ShowErrors(errors);

          m_CustomShaders[key] = ResourceId();

          QString prevtext = ui->customShader->currentText();
          ui->customShader->addItem(fn);
          ui->customShader->setCurrentText(prevtext);
          continue;
        }

        ANALYTIC_SET(UIFeatures.CustomTextureVisualise, true);

        fileHandle.close();

        rdcarray<rdcstr> dirs;

        for(QDir d : getShaderDirectories())
        {
          if(d.exists())
            dirs.push_back(d.absolutePath());
        }

        m_CustomShaders[key] = ResourceId();
        m_CustomShadersBusy.push_back(key);
        m_Ctx.Replay().AsyncInvoke(
            [this, fn, dirs, key, shaderBytes, encoding, errors](IReplayController *r) {
              rdcstr buildErrors;

              r->SetCustomShaderIncludes(dirs);

              ResourceId id;
              rdctie(id, buildErrors) = r->BuildCustomShader(
                  "main", encoding, shaderBytes, ShaderCompileFlags(), ShaderStage::Pixel);

              if(!errors.empty())
                buildErrors = errors + "\n\n" + buildErrors;

              if(m_CustomShaderEditor.contains(key))
              {
                IShaderViewer *editor = m_CustomShaderEditor[key];
                GUIInvoke::call(editor->Widget(),
                                [editor, buildErrors]() { editor->ShowErrors(buildErrors); });
              }

              GUIInvoke::call(this, [this, fn, key, id]() {
                QString prevtext = ui->customShader->currentText();
                ui->customShader->addItem(fn);
                ui->customShader->setCurrentText(prevtext);

                m_CustomShaders[key] = id;
                m_CustomShadersBusy.removeOne(key);

                UI_UpdateChannels();
              });
            });
      }
    }
  }
}

QList<QDir> TextureViewer::getShaderDirectories() const
{
  QList<QDir> dirs;
  dirs.reserve(int(m_Ctx.Config().TextureViewer_ShaderDirs.size() + 1u));
  dirs.append(QDir(ConfigFilePath(QString())));
  for(const rdcstr &dir : m_Ctx.Config().TextureViewer_ShaderDirs)
  {
    dirs.append(QDir(dir));
  }

  return dirs;
}

QString TextureViewer::getShaderPath(const QString &filename) const
{
  QString path;
  QList<QDir> directories = getShaderDirectories();
  for(const QDir &dir : directories)
  {
    QStringList currentDirFiles =
        dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::IgnoreCase);

    if(currentDirFiles.contains(filename, Qt::CaseInsensitive))
    {
      path = QDir::cleanPath(dir.absoluteFilePath(filename));
      break;
    }
  }

  return path;
}

void TextureViewer::on_customCreate_clicked()
{
  QString filename = ui->customShader->currentText();

  if(filename.isEmpty())
  {
    RDDialog::critical(this, tr("Error Creating Shader"),
                       tr("No shader name specified.\nEnter a new name in the textbox"));
    return;
  }

  if(m_CustomShaders.contains(filename.toUpper()))
  {
    RDDialog::critical(this, tr("Error Creating Shader"),
                       tr("Selected shader already exists.\nEnter a new name in the textbox."));
    ui->customShader->setCurrentText(QString());
    UI_UpdateChannels();
    return;
  }

  ShaderEncoding enc = encodingExtensions[QFileInfo(filename).completeSuffix()];

  if(enc == ShaderEncoding::Unknown)
  {
    QString extString;

    for(auto it = encodingExtensions.begin(); it != encodingExtensions.end(); ++it)
    {
      if(!canCompileCustomShader(it.value()))
        continue;

      if(!extString.isEmpty())
        extString += lit(", ");
      extString += it.key();
    }

    RDDialog::critical(this, tr("Error Creating Shader"),
                       tr("No file extension specified, unknown shading language.\n"
                          "Filename must contain one of: %1")
                           .arg(extString));
    return;
  }

  QString src;

  if(enc == ShaderEncoding::HLSL)
  {
    src =
        lit("float4 main(float4 pos : SV_Position, float4 uv : TEXCOORD0) : SV_Target0\n"
            "{\n"
            "    return float4(0,0,0,1);\n"
            "}\n");
  }
  else if(enc == ShaderEncoding::GLSL)
  {
    src =
        lit("#version 420 core\n\n"
            "layout (location = 0) in vec2 uv;\n\n"
            "layout (location = 0) out vec4 color_out;\n\n"
            "void main()\n"
            "{\n"
            "    color_out = vec4(0,0,0,1);\n"
            "}\n");
  }
  else if(enc == ShaderEncoding::SPIRVAsm)
  {
    src = lit("; SPIR-V");
  }
  else
  {
    src = tr("Unknown format - no template available");
  }

  QString path = QDir::cleanPath(QDir(ConfigFilePath(QString())).absoluteFilePath(filename));
  QFile fileHandle(path);
  if(fileHandle.open(QFile::WriteOnly | QIODevice::Truncate | QIODevice::Text))
  {
    fileHandle.write(src.toUtf8());
    fileHandle.close();
  }
  else
  {
    RDDialog::critical(
        this, tr("Cannot create shader"),
        tr("Couldn't create file for shader %1\n%2").arg(filename).arg(fileHandle.errorString()));
  }

  // auto-open edit window
  on_customEdit_clicked();

  reloadCustomShaders(filename);
}

void TextureViewer::on_customEdit_clicked()
{
  QString filename = ui->customShader->currentText();
  QString key = filename.toUpper();

  if(filename.isEmpty())
  {
    RDDialog::critical(this, tr("Error Editing Shader"),
                       tr("No shader selected.\nSelect a custom shader from the drop-down"));
    return;
  }

  QString path = getShaderPath(filename);
  QString src;

  QFile fileHandle(path);
  if(fileHandle.open(QFile::ReadOnly | QFile::Text))
  {
    QTextStream stream(&fileHandle);
    src = stream.readAll();
    fileHandle.close();
  }
  else
  {
    RDDialog::critical(
        this, tr("Cannot open shader"),
        tr("Couldn't open file for shader %1\n%2").arg(filename).arg(fileHandle.errorString()));
    return;
  }

  rdcstrpairs files;
  files.push_back({filename, src});

  QPointer<TextureViewer> thisPointer(this);

  IShaderViewer *s = m_Ctx.EditShader(
      ResourceId(), ShaderStage::Fragment, lit("main"), files,
      encodingExtensions[QFileInfo(filename).completeSuffix()], ShaderCompileFlags(),
      // Save Callback
      [thisPointer, key, filename, path](ICaptureContext *ctx, IShaderViewer *viewer, ResourceId,
                                         ShaderStage, ShaderEncoding, ShaderCompileFlags, rdcstr,
                                         bytebuf) {
        {
          // don't trigger a full refresh
          if(thisPointer)
            thisPointer->m_CustomShaderWriteTime = thisPointer->m_CustomShaderTimer.elapsed();

          rdcstrpairs files = viewer->GetCurrentFileContents();

          if(files.size() != 1)
            qCritical() << "Unexpected number of files in custom shader viewer" << files.count();

          if(files.empty())
            return;

          QFile fileHandle(path);
          if(fileHandle.open(QFile::WriteOnly | QIODevice::Truncate | QIODevice::Text))
          {
            fileHandle.write(files[0].second.c_str(), files[0].second.size());
            fileHandle.close();

            // watcher doesn't trigger on internal modifications
            if(thisPointer)
              thisPointer->reloadCustomShaders(filename);
          }
          else
          {
            if(thisPointer)
            {
              RDDialog::critical(
                  thisPointer, tr("Cannot save shader"),
                  tr("Couldn't save file for shader %1\n%2").arg(filename).arg(fileHandle.errorString()));
            }
          }
        }
      },

      [thisPointer, key](ICaptureContext *, IShaderViewer *, ResourceId) {
        if(thisPointer)
          thisPointer->m_CustomShaderEditor.remove(key);
      });

  m_CustomShaderEditor[key] = s;

  m_Ctx.AddDockWindow(s->Widget(), DockReference::AddTo, this);
}

void TextureViewer::on_customDelete_clicked()
{
  QString shaderName = ui->customShader->currentText();

  if(shaderName.isEmpty())
  {
    RDDialog::critical(this, tr("Error Deleting Shader"),
                       tr("No shader selected.\nSelect a custom shader from the drop-down"));
    return;
  }

  if(!m_CustomShaders.contains(shaderName.toUpper()))
  {
    RDDialog::critical(
        this, tr("Error Deleting Shader"),
        tr("Selected shader doesn't exist.\nSelect a custom shader from the drop-down"));
    return;
  }

  QMessageBox::StandardButton res =
      RDDialog::question(this, tr("Deleting Custom Shader"),
                         tr("Really delete %1?").arg(shaderName), RDDialog::YesNoCancel);

  if(res == QMessageBox::Yes)
  {
    QString path = getShaderPath(shaderName);

    if(!QFileInfo::exists(path))
    {
      RDDialog::critical(
          this, tr("Error Deleting Shader"),
          tr("Shader file %1 can't be found.\nSelect a custom shader from the drop-down")
              .arg(shaderName));
      return;
    }

    if(!QFile::remove(path))
    {
      RDDialog::critical(this, tr("Error Deleting Shader"),
                         tr("Error deleting shader %1 from disk").arg(shaderName));
      return;
    }

    ui->customShader->setCurrentText(QString());
    UI_UpdateChannels();
    reloadCustomShaders(QString());
  }
}

void TextureViewer::customShaderModified(const QString &path)
{
  static bool recurse = false;

  if(recurse)
    return;

  // if we just wrote a shader less than 100ms ago, don't refresh - this will have been handled
  // internally at a finer granularity than 'all shaders'
  if(m_CustomShaderWriteTime > m_CustomShaderTimer.elapsed() - 100)
    return;

  recurse = true;

  // allow time for modifications to finish
  QThread::msleep(15);

  reloadCustomShaders(QString());

  recurse = false;
}
