mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-15 13:57:57 -07:00
fef1b84f0a
QStringLiterals generate a buffer so that during runtime there's very little cost to constructing a QString. However, this also means that duplicated strings cannot be optimized out into a single entry that gets referenced everywhere, taking up space in the binary. Rather than use QStringLiteral(""), we can just use QString{} (the default constructor) to signify the empty string. This gets rid of an unnecessary string buffer from being created, saving a tiny bit of space. While we're at it, we can just use the character overloads of particular functions when they're available instead of using a QString overload. The characters in this case are Latin-1 to begin with, so we can just specify the characters as QLatin1Char instances to use those overloads. These will automatically convert to QChar if needed, so this is safe.
500 lines
14 KiB
C++
500 lines
14 KiB
C++
// Copyright 2018 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include "DolphinQt/FIFO/FIFOAnalyzer.h"
|
|
|
|
#include <QGroupBox>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QListWidget>
|
|
#include <QPushButton>
|
|
#include <QSplitter>
|
|
#include <QTextBrowser>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
|
|
#include "Common/Assert.h"
|
|
#include "Common/Swap.h"
|
|
#include "Core/FifoPlayer/FifoPlayer.h"
|
|
|
|
#include "DolphinQt/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::clicked, this, &FIFOAnalyzer::BeginSearch);
|
|
connect(m_search_next, &QPushButton::clicked, this, &FIFOAnalyzer::FindNext);
|
|
connect(m_search_previous, &QPushButton::clicked, 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, 8, 16, QLatin1Char('0'));
|
|
while (objectdata < stream_end)
|
|
{
|
|
new_label += QStringLiteral("%1").arg(*objectdata++, 2, 16, 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 %1 %2")
|
|
.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.empty())
|
|
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_%1").arg(*(cmddata + 1), 2, 16, QLatin1Char('0')) :
|
|
QString::fromStdString(name);
|
|
text += QLatin1Char{'\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);
|
|
}
|