Merge pull request #6882 from spycrab/qt_fifo_analyzer

Qt/FIFOPlayer: Implement Analyzer
This commit is contained in:
spycrab 2018-05-23 00:03:20 +02:00 committed by GitHub
commit db65e44335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 599 additions and 8 deletions

View File

@ -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

View File

@ -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>
</Project>

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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)

View File

@ -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;
};

View File

@ -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"