dolphin/Source/Core/DolphinQt/FIFO/FIFOAnalyzer.cpp
Pokechu22 2ddf2c3ba2 Update and clear the description on each object change
Since the description updating is tied to the selection changing on the detail list, and the detail list is recreated on each object change, behavior was somewhat broken.  Clearing the list changed the current row to zero, but nothing else (particularly m_object_data_offsets) had been updated, so the description was not necessarily correct (this is easier to observe now since the vertex data is at the end, so it's easier to get different lengths of register updates).  Furthermore, subsequent clears did not update the current row since there was no visible selection, so it only changed the description once.  The current row is now always set to zero, which forces an update (and also scrolls the list back to the top).  The presence of FRAME_ROLE and OBJECT_ROLE are also checked so that the description is cleared if no object is selected.
2021-05-07 15:42:22 -07:00

564 lines
17 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/CPMemory.h"
#include "VideoCommon/OpcodeDecoding.h"
#include "VideoCommon/XFStructs.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, [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;
m_search_next->setEnabled(false);
m_search_previous->setEnabled(false);
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_edit, &QLineEdit::returnPressed, this, &FIFOAnalyzer::BeginSearch);
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();
const u32 frame_count = file->GetFrameCount();
for (u32 frame = 0; frame < frame_count; frame++)
{
auto* frame_item = new QTreeWidgetItem({tr("Frame %1").arg(frame)});
recording_item->addChild(frame_item);
const u32 object_count = FifoPlayer::GetInstance().GetFrameObjectCount(frame);
for (u32 object = 0; object < object_count; object++)
{
auto* object_item = new QTreeWidgetItem({tr("Object %1").arg(object)});
frame_item->addChild(object_item);
object_item->setData(0, FRAME_ROLE, frame);
object_item->setData(0, OBJECT_ROLE, object);
}
}
}
void FIFOAnalyzer::UpdateDetails()
{
// Clearing the detail list can update the selection, which causes UpdateDescription to be called
// immediately. However, the object data offsets have not been recalculated yet, which can cause
// the wrong data to be used, potentially leading to out of bounds data or other bad things.
// Clear m_object_data_offsets first, so that UpdateDescription exits immediately.
m_object_data_offsets.clear();
m_detail_list->clear();
m_search_results.clear();
m_search_next->setEnabled(false);
m_search_previous->setEnabled(false);
m_search_label->clear();
if (!FifoPlayer::GetInstance().IsPlaying())
return;
const auto items = m_tree_widget->selectedItems();
if (items.isEmpty() || items[0]->data(0, OBJECT_ROLE).isNull())
return;
const u32 frame_nr = items[0]->data(0, FRAME_ROLE).toUInt();
const u32 object_nr = items[0]->data(0, OBJECT_ROLE).toUInt();
const auto& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
const auto& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
// Note that frame_info.objectStarts[object_nr] is the start of the primitive data,
// but we want to start with the register updates which happen before that.
const u32 object_start = (object_nr == 0 ? 0 : frame_info.objectEnds[object_nr - 1]);
const u32 object_nonprim_size = frame_info.objectStarts[object_nr] - object_start;
const u32 object_size = frame_info.objectEnds[object_nr] - object_start;
const u8* const object = &fifo_frame.fifoData[object_start];
u32 object_offset = 0;
while (object_offset < object_nonprim_size)
{
QString new_label;
const u32 start_offset = object_offset;
m_object_data_offsets.push_back(start_offset);
const u8 command = object[object_offset++];
switch (command)
{
case OpcodeDecoder::GX_NOP:
new_label = QStringLiteral("NOP");
break;
case OpcodeDecoder::GX_CMD_UNKNOWN_METRICS:
new_label = QStringLiteral("GX_CMD_UNKNOWN_METRICS");
break;
case OpcodeDecoder::GX_CMD_INVL_VC:
new_label = QStringLiteral("GX_CMD_INVL_VC");
break;
case OpcodeDecoder::GX_LOAD_CP_REG:
{
const u8 cmd2 = object[object_offset++];
const u32 value = Common::swap32(&object[object_offset]);
object_offset += 4;
const auto [name, desc] = GetCPRegInfo(cmd2, value);
ASSERT(!name.empty());
new_label = QStringLiteral("CP %1 %2 %3")
.arg(cmd2, 2, 16, QLatin1Char('0'))
.arg(value, 8, 16, QLatin1Char('0'))
.arg(QString::fromStdString(name));
}
break;
case OpcodeDecoder::GX_LOAD_XF_REG:
{
const auto [name, desc] = GetXFTransferInfo(&object[object_offset]);
const u32 cmd2 = Common::swap32(&object[object_offset]);
object_offset += 4;
ASSERT(!name.empty());
const u8 stream_size = ((cmd2 >> 16) & 15) + 1;
new_label = QStringLiteral("XF %1 ").arg(cmd2, 8, 16, QLatin1Char('0'));
for (u8 i = 0; i < stream_size; i++)
{
const u32 value = Common::swap32(&object[object_offset]);
object_offset += 4;
new_label += QStringLiteral("%1 ").arg(value, 8, 16, QLatin1Char('0'));
}
new_label += QStringLiteral(" ") + QString::fromStdString(name);
}
break;
case OpcodeDecoder::GX_LOAD_INDX_A:
new_label = QStringLiteral("LOAD INDX A");
object_offset += 4;
break;
case OpcodeDecoder::GX_LOAD_INDX_B:
new_label = QStringLiteral("LOAD INDX B");
object_offset += 4;
break;
case OpcodeDecoder::GX_LOAD_INDX_C:
new_label = QStringLiteral("LOAD INDX C");
object_offset += 4;
break;
case OpcodeDecoder::GX_LOAD_INDX_D:
new_label = QStringLiteral("LOAD INDX D");
object_offset += 4;
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);
object_offset += 8;
new_label = QStringLiteral("CALL DL");
break;
case OpcodeDecoder::GX_LOAD_BP_REG:
{
const u8 cmd2 = object[object_offset++];
const u32 cmddata = Common::swap24(&object[object_offset]);
object_offset += 3;
const auto [name, desc] = GetBPRegInfo(cmd2, cmddata);
ASSERT(!name.empty());
new_label = QStringLiteral("BP %1 %2 %3")
.arg(cmd2, 2, 16, QLatin1Char('0'))
.arg(cmddata, 6, 16, QLatin1Char('0'))
.arg(QString::fromStdString(name));
}
break;
default:
new_label = tr("Unexpected 0x80 call? Aborting...");
object_offset = object_nonprim_size;
break;
}
new_label = QStringLiteral("%1: ").arg(object_start + start_offset, 8, 16, QLatin1Char('0')) +
new_label;
m_detail_list->addItem(new_label);
}
// Object primitive data
ASSERT(object_offset == object_nonprim_size);
m_object_data_offsets.push_back(object_offset);
const u8 cmd = object[object_offset++];
const u16 vertex_count = Common::swap16(&object[object_offset]);
object_offset += 2;
const u32 object_prim_size = object_size - object_offset;
QString new_label = QStringLiteral("%1: %2 %3 ")
.arg(object_start + object_offset, 8, 16, QLatin1Char('0'))
.arg(cmd, 2, 16, QLatin1Char('0'))
.arg(vertex_count, 4, 16, QLatin1Char('0'));
while (object_offset < object_size)
{
u32 byte = object[object_offset++];
new_label += QStringLiteral("%1").arg(byte, 2, 16, QLatin1Char('0'));
}
if (vertex_count != 0 && (object_prim_size % vertex_count) != 0)
{
new_label += QLatin1Char{'\n'};
new_label += tr("NOTE: Stream size doesn't match actual data length");
}
m_detail_list->addItem(new_label);
// Needed to ensure the description updates when changing objects
m_detail_list->setCurrentRow(0);
}
void FIFOAnalyzer::BeginSearch()
{
const QString search_str = m_search_edit->text();
if (!FifoPlayer::GetInstance().IsPlaying())
return;
const auto items = m_tree_widget->selectedItems();
if (items.isEmpty() || items[0]->data(0, FRAME_ROLE).isNull() ||
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();
const u32 frame_nr = items[0]->data(0, FRAME_ROLE).toUInt();
const u32 object_nr = items[0]->data(0, OBJECT_ROLE).toUInt();
const AnalyzedFrameInfo& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
const FifoFrameInfo& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
const u32 object_start = (object_nr == 0 ? 0 : frame_info.objectEnds[object_nr - 1]);
const u32 object_size = frame_info.objectEnds[object_nr] - object_start;
const u8* const object = &fifo_frame.fifoData[object_start];
// TODO: Support searching for bit patterns
for (u32 cmd_nr = 0; cmd_nr < m_object_data_offsets.size(); cmd_nr++)
{
const u32 cmd_start = m_object_data_offsets[cmd_nr];
const u32 cmd_end = (cmd_nr + 1 == m_object_data_offsets.size()) ?
object_size :
m_object_data_offsets[cmd_nr + 1];
const u8* const cmd_start_ptr = &object[cmd_start];
const u8* const cmd_end_ptr = &object[cmd_end];
for (const u8* ptr = cmd_start_ptr; ptr < cmd_end_ptr - length + 1; ptr++)
{
if (std::equal(search_val.begin(), search_val.end(), ptr))
{
m_search_results.emplace_back(frame_nr, object_nr, cmd_nr);
break;
}
}
}
ShowSearchResult(0);
m_search_label->setText(
tr("Found %1 results for \"%2\"").arg(m_search_results.size()).arg(search_str));
}
void FIFOAnalyzer::FindNext()
{
const int index = m_detail_list->currentRow();
ASSERT(index >= 0);
auto next_result =
std::find_if(m_search_results.begin(), m_search_results.end(),
[index](auto& result) { return result.m_cmd > static_cast<u32>(index); });
if (next_result != m_search_results.end())
{
ShowSearchResult(next_result - m_search_results.begin());
}
}
void FIFOAnalyzer::FindPrevious()
{
const int index = m_detail_list->currentRow();
ASSERT(index >= 0);
auto prev_result =
std::find_if(m_search_results.rbegin(), m_search_results.rend(),
[index](auto& result) { return result.m_cmd < static_cast<u32>(index); });
if (prev_result != m_search_results.rend())
{
ShowSearchResult((m_search_results.rend() - prev_result) - 1);
}
}
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.m_frame)->child(result.m_object);
m_tree_widget->setCurrentItem(object_item);
m_detail_list->setCurrentRow(result.m_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();
if (!FifoPlayer::GetInstance().IsPlaying())
return;
const auto items = m_tree_widget->selectedItems();
if (items.isEmpty() || m_object_data_offsets.empty())
return;
if (items[0]->data(0, FRAME_ROLE).isNull() || items[0]->data(0, OBJECT_ROLE).isNull())
return;
const u32 frame_nr = items[0]->data(0, FRAME_ROLE).toUInt();
const u32 object_nr = items[0]->data(0, OBJECT_ROLE).toUInt();
const u32 entry_nr = m_detail_list->currentRow();
const AnalyzedFrameInfo& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
const FifoFrameInfo& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
const u32 object_start = (object_nr == 0 ? 0 : frame_info.objectEnds[object_nr - 1]);
const u8* cmddata = &fifo_frame.fifoData[object_start + 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)
{
const u8 cmd = *(cmddata + 1);
const u32 value = Common::swap24(cmddata + 2);
const auto [name, desc] = GetBPRegInfo(cmd, value);
ASSERT(!name.empty());
text = tr("BP register ");
text += 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)
{
const u8 cmd = *(cmddata + 1);
const u32 value = Common::swap32(cmddata + 2);
const auto [name, desc] = GetCPRegInfo(cmd, value);
ASSERT(!name.empty());
text = tr("CP register ");
text += 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_XF_REG)
{
const auto [name, desc] = GetXFTransferInfo(cmddata + 1);
ASSERT(!name.empty());
text = tr("XF register ");
text += QString::fromStdString(name);
text += QLatin1Char{'\n'};
if (desc.empty())
text += tr("No description available");
else
text += QString::fromStdString(desc);
}
else
{
text = tr("No description available");
}
m_entry_detail_browser->setText(text);
}