From 94e97d47a07a46550f48009a5dac69b616617ce3 Mon Sep 17 00:00:00 2001
From: spycrab <spycrab@users.noreply.github.com>
Date: Mon, 14 May 2018 22:01:23 +0200
Subject: [PATCH] Qt/FIFOPlayer: Implement Analyzer

---
 Source/Core/DolphinQt2/CMakeLists.txt         |   3 +-
 Source/Core/DolphinQt2/DolphinQt2.vcxproj     |  11 +-
 Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.cpp  | 500 ++++++++++++++++++
 Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.h    |  67 +++
 .../{ => FIFO}/FIFOPlayerWindow.cpp           |  21 +-
 .../DolphinQt2/{ => FIFO}/FIFOPlayerWindow.h  |   3 +
 Source/Core/DolphinQt2/MainWindow.cpp         |   2 +-
 7 files changed, 599 insertions(+), 8 deletions(-)
 create mode 100644 Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.cpp
 create mode 100644 Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.h
 rename Source/Core/DolphinQt2/{ => FIFO}/FIFOPlayerWindow.cpp (95%)
 rename Source/Core/DolphinQt2/{ => FIFO}/FIFOPlayerWindow.h (96%)

diff --git a/Source/Core/DolphinQt2/CMakeLists.txt b/Source/Core/DolphinQt2/CMakeLists.txt
index 3d205741b3..693bbc2885 100644
--- a/Source/Core/DolphinQt2/CMakeLists.txt
+++ b/Source/Core/DolphinQt2/CMakeLists.txt
@@ -8,7 +8,8 @@ set(CMAKE_AUTOMOC ON)
 add_executable(dolphin-emu
   AboutDialog.cpp
   CheatsManager.cpp
-  FIFOPlayerWindow.cpp
+  FIFO/FIFOPlayerWindow.cpp
+  FIFO/FIFOAnalyzer.cpp
   HotkeyScheduler.cpp
   Host.cpp
   Main.cpp
diff --git a/Source/Core/DolphinQt2/DolphinQt2.vcxproj b/Source/Core/DolphinQt2/DolphinQt2.vcxproj
index 9a3334767a..decbae647a 100644
--- a/Source/Core/DolphinQt2/DolphinQt2.vcxproj
+++ b/Source/Core/DolphinQt2/DolphinQt2.vcxproj
@@ -44,7 +44,7 @@
       <AdditionalDependencies>avrt.lib;iphlpapi.lib;winmm.lib;setupapi.lib;opengl32.lib;glu32.lib;rpcrt4.lib;comctl32.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
     <ClCompile>
-      <AdditionalIncludeDirectories>$(ProjectDir)VideoInterface;$(ProjectDir)GameList;$(ProjectDir)Debugger;$(ProjectDir)Settings;$(ProjectDir)Config;$(ProjectDir)Config\Mapping;$(ProjectDir)Config\Graphics;$(ProjectDir)NetPlay;$(ProjectDir)QtUtils;$(ProjectDir)TAS;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(ProjectDir)VideoInterface;$(ProjectDir)GameList;$(ProjectDir)Debugger;$(ProjectDir)Settings;$(ProjectDir)Config;$(ProjectDir)Config\Mapping;$(ProjectDir)Config\Graphics;$(ProjectDir)NetPlay;$(ProjectDir)QtUtils;$(ProjectDir)TAS;$(ProjectDir)FIFO;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
     </ClCompile>
     <Manifest>
       <AdditionalManifestFiles>DolphinQt2.manifest;%(AdditionalManifestFiles)</AdditionalManifestFiles>
@@ -105,7 +105,8 @@
     <QtMoc Include="Config\PatchesWidget.h" />
     <QtMoc Include="Config\PropertiesDialog.h" />
     <QtMoc Include="Config\SettingsWindow.h" />
-    <QtMoc Include="FIFOPlayerWindow.h" />
+    <QtMoc Include="FIFO\FIFOAnalyzer.h" />
+    <QtMoc Include="FIFO\FIFOPlayerWindow.h" />
     <QtMoc Include="TAS\GCTASInputWindow.h" />
     <QtMoc Include="TAS\WiiTASInputWindow.h" />
     <QtMoc Include="TAS\StickWidget.h" />
@@ -173,6 +174,7 @@
     <ClCompile Include="$(QtMocOutPrefix)DoubleClickEventFilter.cpp" />
     <ClCompile Include="$(QtMocOutPrefix)ElidedButton.cpp" />
     <ClCompile Include="$(QtMocOutPrefix)EnhancementsWidget.cpp" />
+    <ClCompile Include="$(QtMocOutPrefix)FIFOAnalyzer.cpp" />
     <ClCompile Include="$(QtMocOutPrefix)FIFOPlayerWindow.cpp" />
     <ClCompile Include="$(QtMocOutPrefix)FilesystemWidget.cpp" />
     <ClCompile Include="$(QtMocOutPrefix)GCKeyboardEmu.cpp" />
@@ -307,7 +309,8 @@
     <ClCompile Include="Debugger\JITWidget.cpp" />
     <ClCompile Include="Debugger\MemoryWidget.cpp" />
     <ClCompile Include="Debugger\MemoryViewWidget.cpp" />
-    <ClCompile Include="FIFOPlayerWindow.cpp" />
+    <ClCompile Include="FIFO\FIFOAnalyzer.cpp" />
+    <ClCompile Include="FIFO\FIFOPlayerWindow.cpp" />
     <ClCompile Include="QtUtils\WinIconHelper.cpp" />
     <ClCompile Include="TAS\GCTASInputWindow.cpp" />
     <ClCompile Include="TAS\WiiTASInputWindow.cpp" />
@@ -456,4 +459,4 @@
     <Message Text="Copy: @(BinaryFiles) -&gt; $(BinaryOutputDir)" Importance="High" />
     <Copy SourceFiles="@(BinaryFiles)" DestinationFolder="$(BinaryOutputDir)" />
   </Target>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.cpp b/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.cpp
new file mode 100644
index 0000000000..7e8323505d
--- /dev/null
+++ b/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.cpp
@@ -0,0 +1,500 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DolphinQt2/FIFO/FIFOAnalyzer.h"
+
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QLabel>
+#include <QLineEdit>
+#include <QListWidget>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QSplitter>
+#include <QTextBrowser>
+#include <QTreeWidget>
+#include <QTreeWidgetItem>
+
+#include "Common/Assert.h"
+#include "Common/Swap.h"
+#include "Core/FifoPlayer/FifoPlayer.h"
+
+#include "DolphinQt2/Settings.h"
+
+#include "VideoCommon/BPMemory.h"
+#include "VideoCommon/OpcodeDecoding.h"
+
+constexpr int FRAME_ROLE = Qt::UserRole;
+constexpr int OBJECT_ROLE = Qt::UserRole + 1;
+
+FIFOAnalyzer::FIFOAnalyzer()
+{
+  CreateWidgets();
+  ConnectWidgets();
+
+  UpdateTree();
+
+  auto& settings = Settings::GetQSettings();
+
+  m_object_splitter->restoreState(
+      settings.value(QStringLiteral("fifoanalyzer/objectsplitter")).toByteArray());
+  m_search_splitter->restoreState(
+      settings.value(QStringLiteral("fifoanalyzer/searchsplitter")).toByteArray());
+
+  m_detail_list->setFont(Settings::Instance().GetDebugFont());
+  m_entry_detail_browser->setFont(Settings::Instance().GetDebugFont());
+
+  connect(&Settings::Instance(), &Settings::DebugFontChanged, [this] {
+    m_detail_list->setFont(Settings::Instance().GetDebugFont());
+    m_entry_detail_browser->setFont(Settings::Instance().GetDebugFont());
+  });
+}
+
+FIFOAnalyzer::~FIFOAnalyzer()
+{
+  auto& settings = Settings::GetQSettings();
+
+  settings.setValue(QStringLiteral("fifoanalyzer/objectsplitter"), m_object_splitter->saveState());
+  settings.setValue(QStringLiteral("fifoanalyzer/searchsplitter"), m_search_splitter->saveState());
+}
+
+void FIFOAnalyzer::CreateWidgets()
+{
+  m_tree_widget = new QTreeWidget;
+  m_detail_list = new QListWidget;
+  m_entry_detail_browser = new QTextBrowser;
+
+  m_object_splitter = new QSplitter(Qt::Horizontal);
+
+  m_object_splitter->addWidget(m_tree_widget);
+  m_object_splitter->addWidget(m_detail_list);
+
+  m_tree_widget->header()->hide();
+
+  m_search_box = new QGroupBox(tr("Search Current Object"));
+  m_search_edit = new QLineEdit;
+  m_search_new = new QPushButton(tr("Search"));
+  m_search_next = new QPushButton(tr("Next Match"));
+  m_search_previous = new QPushButton(tr("Previous Match"));
+  m_search_label = new QLabel;
+
+  auto* box_layout = new QHBoxLayout;
+
+  box_layout->addWidget(m_search_edit);
+  box_layout->addWidget(m_search_new);
+  box_layout->addWidget(m_search_next);
+  box_layout->addWidget(m_search_previous);
+  box_layout->addWidget(m_search_label);
+
+  m_search_box->setLayout(box_layout);
+
+  m_search_box->setMaximumHeight(m_search_box->minimumSizeHint().height());
+
+  m_search_splitter = new QSplitter(Qt::Vertical);
+
+  m_search_splitter->addWidget(m_object_splitter);
+  m_search_splitter->addWidget(m_entry_detail_browser);
+  m_search_splitter->addWidget(m_search_box);
+
+  auto* layout = new QHBoxLayout;
+  layout->addWidget(m_search_splitter);
+
+  setLayout(layout);
+}
+
+void FIFOAnalyzer::ConnectWidgets()
+{
+  connect(m_tree_widget, &QTreeWidget::itemSelectionChanged, this, &FIFOAnalyzer::UpdateDetails);
+  connect(m_detail_list, &QListWidget::itemSelectionChanged, this,
+          &FIFOAnalyzer::UpdateDescription);
+
+  connect(m_search_new, &QPushButton::pressed, this, &FIFOAnalyzer::BeginSearch);
+  connect(m_search_next, &QPushButton::pressed, this, &FIFOAnalyzer::FindNext);
+  connect(m_search_previous, &QPushButton::pressed, this, &FIFOAnalyzer::FindPrevious);
+}
+
+void FIFOAnalyzer::Update()
+{
+  UpdateTree();
+  UpdateDetails();
+  UpdateDescription();
+}
+
+void FIFOAnalyzer::UpdateTree()
+{
+  m_tree_widget->clear();
+
+  if (!FifoPlayer::GetInstance().IsPlaying())
+  {
+    m_tree_widget->addTopLevelItem(new QTreeWidgetItem({tr("No recording loaded.")}));
+    return;
+  }
+
+  auto* recording_item = new QTreeWidgetItem({tr("Recording")});
+
+  m_tree_widget->addTopLevelItem(recording_item);
+
+  auto* file = FifoPlayer::GetInstance().GetFile();
+
+  int object_count = FifoPlayer::GetInstance().GetFrameObjectCount();
+  int frame_count = file->GetFrameCount();
+
+  for (int i = 0; i < frame_count; i++)
+  {
+    auto* frame_item = new QTreeWidgetItem({tr("Frame %1").arg(i)});
+
+    recording_item->addChild(frame_item);
+
+    for (int j = 0; j < object_count; j++)
+    {
+      auto* object_item = new QTreeWidgetItem({tr("Object %1").arg(j)});
+
+      frame_item->addChild(object_item);
+
+      object_item->setData(0, FRAME_ROLE, i);
+      object_item->setData(0, OBJECT_ROLE, j);
+    }
+  }
+}
+
+void FIFOAnalyzer::UpdateDetails()
+{
+  m_detail_list->clear();
+  m_object_data_offsets.clear();
+
+  auto items = m_tree_widget->selectedItems();
+
+  if (items.isEmpty() || items[0]->data(0, OBJECT_ROLE).isNull())
+    return;
+
+  int frame_nr = items[0]->data(0, FRAME_ROLE).toInt();
+  int object_nr = items[0]->data(0, OBJECT_ROLE).toInt();
+
+  const auto& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
+  const auto& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
+
+  const u8* objectdata_start = &fifo_frame.fifoData[frame_info.objectStarts[object_nr]];
+  const u8* objectdata_end = &fifo_frame.fifoData[frame_info.objectEnds[object_nr]];
+  const u8* objectdata = objectdata_start;
+  const std::ptrdiff_t obj_offset =
+      objectdata_start - &fifo_frame.fifoData[frame_info.objectStarts[0]];
+
+  int cmd = *objectdata++;
+  int stream_size = Common::swap16(objectdata);
+  objectdata += 2;
+  QString new_label = QStringLiteral("%1:  %2 %3  ")
+                          .arg(obj_offset, 8, 16, QLatin1Char('0'))
+                          .arg(cmd, 2, 16, QLatin1Char('0'))
+                          .arg(stream_size, 4, 16, QLatin1Char('0'));
+  if (stream_size && ((objectdata_end - objectdata) % stream_size))
+    new_label += tr("NOTE: Stream size doesn't match actual data length\n");
+
+  while (objectdata < objectdata_end)
+    new_label += QStringLiteral("%1").arg(*objectdata++, 2, 16, QLatin1Char('0'));
+
+  m_detail_list->addItem(new_label);
+  m_object_data_offsets.push_back(0);
+
+  // Between objectdata_end and next_objdata_start, there are register setting commands
+  if (object_nr + 1 < static_cast<int>(frame_info.objectStarts.size()))
+  {
+    const u8* next_objdata_start = &fifo_frame.fifoData[frame_info.objectStarts[object_nr + 1]];
+    while (objectdata < next_objdata_start)
+    {
+      m_object_data_offsets.push_back(objectdata - objectdata_start);
+      int new_offset = objectdata - &fifo_frame.fifoData[frame_info.objectStarts[0]];
+      int command = *objectdata++;
+      switch (command)
+      {
+      case OpcodeDecoder::GX_NOP:
+        new_label = QStringLiteral("NOP");
+        break;
+
+      case 0x44:
+        new_label = QStringLiteral("0x44");
+        break;
+
+      case OpcodeDecoder::GX_CMD_INVL_VC:
+        new_label = QStringLiteral("GX_CMD_INVL_VC");
+        break;
+
+      case OpcodeDecoder::GX_LOAD_CP_REG:
+      {
+        u32 cmd2 = *objectdata++;
+        u32 value = Common::swap32(objectdata);
+        objectdata += 4;
+
+        new_label = QStringLiteral("CP  %1  %2")
+                        .arg(cmd2, 2, 16, QLatin1Char('0'))
+                        .arg(value, 8, 16, QLatin1Char('0'));
+      }
+      break;
+
+      case OpcodeDecoder::GX_LOAD_XF_REG:
+      {
+        u32 cmd2 = Common::swap32(objectdata);
+        objectdata += 4;
+
+        u8 streamSize = ((cmd2 >> 16) & 15) + 1;
+
+        const u8* stream_start = objectdata;
+        const u8* stream_end = stream_start + streamSize * 4;
+
+        new_label = QStringLiteral("XF  %1  ").arg(cmd2, 16, 8, QLatin1Char('0'));
+        while (objectdata < stream_end)
+        {
+          new_label += QStringLiteral("%1").arg(*objectdata++, 16, 2, QLatin1Char('0'));
+
+          if (((objectdata - stream_start) % 4) == 0)
+            new_label += QLatin1Char(' ');
+        }
+      }
+      break;
+
+      case OpcodeDecoder::GX_LOAD_INDX_A:
+      case OpcodeDecoder::GX_LOAD_INDX_B:
+      case OpcodeDecoder::GX_LOAD_INDX_C:
+      case OpcodeDecoder::GX_LOAD_INDX_D:
+      {
+        objectdata += 4;
+        new_label = (command == OpcodeDecoder::GX_LOAD_INDX_A) ?
+                        QStringLiteral("LOAD INDX A") :
+                        (command == OpcodeDecoder::GX_LOAD_INDX_B) ?
+                        QStringLiteral("LOAD INDX B") :
+                        (command == OpcodeDecoder::GX_LOAD_INDX_C) ? QStringLiteral("LOAD INDX C") :
+                                                                     QStringLiteral("LOAD INDX D");
+      }
+      break;
+
+      case OpcodeDecoder::GX_CMD_CALL_DL:
+        // The recorder should have expanded display lists into the fifo stream and skipped the
+        // call to start them
+        // That is done to make it easier to track where memory is updated
+        ASSERT(false);
+        objectdata += 8;
+        new_label = QStringLiteral("CALL DL");
+        break;
+
+      case OpcodeDecoder::GX_LOAD_BP_REG:
+      {
+        u32 cmd2 = Common::swap32(objectdata);
+        objectdata += 4;
+        new_label = QStringLiteral("BP  %02X %06X")
+                        .arg(cmd2 >> 24, 2, 16, QLatin1Char('0'))
+                        .arg(cmd2 & 0xFFFFFF, 6, 16, QLatin1Char('0'));
+      }
+      break;
+
+      default:
+        new_label = tr("Unexpected 0x80 call? Aborting...");
+        objectdata = static_cast<const u8*>(next_objdata_start);
+        break;
+      }
+      new_label = QStringLiteral("%1:  ").arg(new_offset, 8, 16, QLatin1Char('0')) + new_label;
+      m_detail_list->addItem(new_label);
+    }
+  }
+}
+
+void FIFOAnalyzer::BeginSearch()
+{
+  QString search_str = m_search_edit->text();
+
+  auto items = m_tree_widget->selectedItems();
+
+  if (items.isEmpty() || items[0]->data(0, FRAME_ROLE).isNull())
+    return;
+
+  if (items[0]->data(0, OBJECT_ROLE).isNull())
+  {
+    m_search_label->setText(tr("Invalid search parameters (no object selected)"));
+    return;
+  }
+
+  // TODO: Remove even string length limit
+  if (search_str.length() % 2)
+  {
+    m_search_label->setText(tr("Invalid search string (only even string lengths supported)"));
+    return;
+  }
+
+  const size_t length = search_str.length() / 2;
+
+  std::vector<u8> search_val;
+
+  for (size_t i = 0; i < length; i++)
+  {
+    const QString byte_str = search_str.mid(static_cast<int>(i * 2), 2);
+
+    bool good;
+    u8 value = byte_str.toUInt(&good, 16);
+
+    if (!good)
+    {
+      m_search_label->setText(tr("Invalid search string (couldn't convert to number)"));
+      return;
+    }
+
+    search_val.push_back(value);
+  }
+
+  m_search_results.clear();
+
+  int frame_nr = items[0]->data(0, FRAME_ROLE).toInt();
+  int object_nr = items[0]->data(0, OBJECT_ROLE).toInt();
+
+  const AnalyzedFrameInfo& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
+  const FifoFrameInfo& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
+
+  // TODO: Support searching through the last object...how do we know where the cmd data ends?
+  // TODO: Support searching for bit patterns
+
+  const auto* start_ptr = &fifo_frame.fifoData[frame_info.objectStarts[object_nr]];
+  const auto* end_ptr = &fifo_frame.fifoData[frame_info.objectStarts[object_nr + 1]];
+
+  for (const u8* ptr = start_ptr; ptr < end_ptr - length + 1; ++ptr)
+  {
+    if (std::equal(search_val.begin(), search_val.end(), ptr))
+    {
+      SearchResult result;
+      result.frame = frame_nr;
+
+      result.object = object_nr;
+      result.cmd = 0;
+      for (unsigned int cmd_nr = 1; cmd_nr < m_object_data_offsets.size(); ++cmd_nr)
+      {
+        if (ptr < start_ptr + m_object_data_offsets[cmd_nr])
+        {
+          result.cmd = cmd_nr - 1;
+          break;
+        }
+      }
+      m_search_results.push_back(result);
+    }
+  }
+
+  ShowSearchResult(0);
+
+  m_search_label->setText(
+      tr("Found %1 results for \"%2\"").arg(m_search_results.size()).arg(search_str));
+}
+
+void FIFOAnalyzer::FindNext()
+{
+  int index = m_detail_list->currentRow();
+
+  if (index == -1)
+  {
+    ShowSearchResult(0);
+    return;
+  }
+
+  for (auto it = m_search_results.begin(); it != m_search_results.end(); ++it)
+  {
+    if (it->cmd > index)
+    {
+      ShowSearchResult(it - m_search_results.begin());
+      return;
+    }
+  }
+}
+
+void FIFOAnalyzer::FindPrevious()
+{
+  int index = m_detail_list->currentRow();
+
+  if (index == -1)
+  {
+    ShowSearchResult(m_search_results.size() - 1);
+    return;
+  }
+
+  for (auto it = m_search_results.rbegin(); it != m_search_results.rend(); ++it)
+  {
+    if (it->cmd < index)
+    {
+      ShowSearchResult(m_search_results.size() - 1 - (it - m_search_results.rbegin()));
+      return;
+    }
+  }
+}
+
+void FIFOAnalyzer::ShowSearchResult(size_t index)
+{
+  if (!m_search_results.size())
+    return;
+
+  if (index > m_search_results.size())
+  {
+    ShowSearchResult(m_search_results.size() - 1);
+    return;
+  }
+
+  const auto& result = m_search_results[index];
+
+  QTreeWidgetItem* object_item =
+      m_tree_widget->topLevelItem(0)->child(result.frame)->child(result.object);
+
+  m_tree_widget->setCurrentItem(object_item);
+  m_detail_list->setCurrentRow(result.cmd);
+
+  m_search_next->setEnabled(index + 1 < m_search_results.size());
+  m_search_previous->setEnabled(index > 0);
+}
+
+void FIFOAnalyzer::UpdateDescription()
+{
+  m_entry_detail_browser->clear();
+
+  auto items = m_tree_widget->selectedItems();
+
+  if (items.isEmpty())
+    return;
+
+  int frame_nr = items[0]->data(0, FRAME_ROLE).toInt();
+  int object_nr = items[0]->data(0, OBJECT_ROLE).toInt();
+  int entry_nr = m_detail_list->currentRow();
+
+  const AnalyzedFrameInfo& frame = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
+  const FifoFrameInfo& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
+
+  const u8* cmddata =
+      &fifo_frame.fifoData[frame.objectStarts[object_nr]] + m_object_data_offsets[entry_nr];
+
+  // TODO: Not sure whether we should bother translating the descriptions
+
+  QString text;
+  if (*cmddata == OpcodeDecoder::GX_LOAD_BP_REG)
+  {
+    std::string name;
+    std::string desc;
+    GetBPRegInfo(cmddata + 1, &name, &desc);
+
+    text = tr("BP register ");
+    text += name.empty() ?
+                QStringLiteral("UNKNOWN_%02X").arg(*(cmddata + 1), 2, 16, QLatin1Char('0')) :
+                QString::fromStdString(name);
+    text += QStringLiteral("\n");
+
+    if (desc.empty())
+      text += tr("No description available");
+    else
+      text += QString::fromStdString(desc);
+  }
+  else if (*cmddata == OpcodeDecoder::GX_LOAD_CP_REG)
+  {
+    text = tr("CP register ");
+  }
+  else if (*cmddata == OpcodeDecoder::GX_LOAD_XF_REG)
+  {
+    text = tr("XF register ");
+  }
+  else
+  {
+    text = tr("No description available");
+  }
+
+  m_entry_detail_browser->setText(text);
+}
diff --git a/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.h b/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.h
new file mode 100644
index 0000000000..f31ddde2a9
--- /dev/null
+++ b/Source/Core/DolphinQt2/FIFO/FIFOAnalyzer.h
@@ -0,0 +1,67 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <vector>
+
+#include <QWidget>
+
+class QGroupBox;
+class QLabel;
+class QLineEdit;
+class QListWidget;
+class QPushButton;
+class QSplitter;
+class QTextBrowser;
+class QTreeWidget;
+
+class FIFOAnalyzer final : public QWidget
+{
+  Q_OBJECT
+
+public:
+  explicit FIFOAnalyzer();
+  ~FIFOAnalyzer();
+
+  void Update();
+
+private:
+  void CreateWidgets();
+  void ConnectWidgets();
+
+  void BeginSearch();
+  void FindNext();
+  void FindPrevious();
+
+  void ShowSearchResult(size_t index);
+
+  void UpdateTree();
+  void UpdateDetails();
+  void UpdateDescription();
+
+  QTreeWidget* m_tree_widget;
+  QListWidget* m_detail_list;
+  QTextBrowser* m_entry_detail_browser;
+  QSplitter* m_object_splitter;
+
+  // Search
+  QGroupBox* m_search_box;
+  QLineEdit* m_search_edit;
+  QPushButton* m_search_new;
+  QPushButton* m_search_next;
+  QPushButton* m_search_previous;
+  QLabel* m_search_label;
+  QSplitter* m_search_splitter;
+
+  struct SearchResult
+  {
+    int frame;
+    int object;
+    int cmd;
+  };
+
+  std::vector<int> m_object_data_offsets;
+  std::vector<SearchResult> m_search_results;
+};
diff --git a/Source/Core/DolphinQt2/FIFOPlayerWindow.cpp b/Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.cpp
similarity index 95%
rename from Source/Core/DolphinQt2/FIFOPlayerWindow.cpp
rename to Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.cpp
index 3f95d2445e..48173c8185 100644
--- a/Source/Core/DolphinQt2/FIFOPlayerWindow.cpp
+++ b/Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.cpp
@@ -2,7 +2,7 @@
 // Licensed under GPLv2+
 // Refer to the license.txt file included.
 
-#include "DolphinQt2/FIFOPlayerWindow.h"
+#include "DolphinQt2/FIFO/FIFOPlayerWindow.h"
 
 #include <QCheckBox>
 #include <QDialogButtonBox>
@@ -13,6 +13,7 @@
 #include <QMessageBox>
 #include <QPushButton>
 #include <QSpinBox>
+#include <QTabWidget>
 #include <QVBoxLayout>
 
 #include <algorithm>
@@ -23,6 +24,7 @@
 #include "Core/FifoPlayer/FifoPlayer.h"
 #include "Core/FifoPlayer/FifoRecorder.h"
 
+#include "DolphinQt2/FIFO/FIFOAnalyzer.h"
 #include "DolphinQt2/QtUtils/QueueOnObject.h"
 #include "DolphinQt2/Settings.h"
 
@@ -142,7 +144,20 @@ void FIFOPlayerWindow::CreateWidgets()
   layout->addWidget(recording_group);
   layout->addWidget(m_button_box);
 
-  setLayout(layout);
+  QWidget* main_widget = new QWidget(this);
+  main_widget->setLayout(layout);
+
+  auto* tab_widget = new QTabWidget(this);
+
+  m_analyzer = new FIFOAnalyzer;
+
+  tab_widget->addTab(main_widget, tr("Play / Record"));
+  tab_widget->addTab(m_analyzer, tr("Analyze"));
+
+  auto* tab_layout = new QVBoxLayout;
+  tab_layout->addWidget(tab_widget);
+
+  setLayout(tab_layout);
 }
 
 void FIFOPlayerWindow::ConnectWidgets()
@@ -292,6 +307,8 @@ void FIFOPlayerWindow::OnFIFOLoaded()
   UpdateInfo();
   UpdateLimits();
   UpdateControls();
+
+  m_analyzer->Update();
 }
 
 void FIFOPlayerWindow::OnEarlyMemoryUpdatesChanged(bool enabled)
diff --git a/Source/Core/DolphinQt2/FIFOPlayerWindow.h b/Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.h
similarity index 96%
rename from Source/Core/DolphinQt2/FIFOPlayerWindow.h
rename to Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.h
index 4f422aa8f0..5441b38aba 100644
--- a/Source/Core/DolphinQt2/FIFOPlayerWindow.h
+++ b/Source/Core/DolphinQt2/FIFO/FIFOPlayerWindow.h
@@ -11,6 +11,7 @@ class QDialogButtonBox;
 class QLabel;
 class QPushButton;
 class QSpinBox;
+class FIFOAnalyzer;
 
 class FIFOPlayerWindow : public QDialog
 {
@@ -59,4 +60,6 @@ private:
   QLabel* m_object_range_to_label;
   QCheckBox* m_early_memory_updates;
   QDialogButtonBox* m_button_box;
+
+  FIFOAnalyzer* m_analyzer;
 };
diff --git a/Source/Core/DolphinQt2/MainWindow.cpp b/Source/Core/DolphinQt2/MainWindow.cpp
index 90bf1b2d20..66129e1fb6 100644
--- a/Source/Core/DolphinQt2/MainWindow.cpp
+++ b/Source/Core/DolphinQt2/MainWindow.cpp
@@ -60,7 +60,7 @@
 #include "DolphinQt2/Debugger/MemoryWidget.h"
 #include "DolphinQt2/Debugger/RegisterWidget.h"
 #include "DolphinQt2/Debugger/WatchWidget.h"
-#include "DolphinQt2/FIFOPlayerWindow.h"
+#include "DolphinQt2/FIFO/FIFOPlayerWindow.h"
 #include "DolphinQt2/GCMemcardManager.h"
 #include "DolphinQt2/Host.h"
 #include "DolphinQt2/HotkeyScheduler.h"