mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-03 22:31:22 +01:00
df81210e96
BPMEM_TEV_COLOR_ENV + 6 (0xC6) was missing due to a typo. BPMEM_BP_MASK (0xFE) does not lend itself well to documentation with the current FIFO analyzer implementation (since it requires remembering the values in BP memory) but still shouldn't be treated as unknown. BPMEM_TX_SETMODE0_4 and BPMEM_TX_SETMODE1_4 (0xA4-0xAB) were missing entirely.
538 lines
16 KiB
C++
538 lines
16 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;
|
|
|
|
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 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:
|
|
{
|
|
u32 cmd2 = *objectdata++;
|
|
u32 value = Common::swap32(objectdata);
|
|
objectdata += 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(objectdata);
|
|
u32 cmd2 = Common::swap32(objectdata);
|
|
objectdata += 4;
|
|
ASSERT(!name.empty());
|
|
|
|
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(' ');
|
|
}
|
|
|
|
new_label += QStringLiteral(" ") + QString::fromStdString(name);
|
|
}
|
|
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:
|
|
{
|
|
const u8 cmd2 = *objectdata++;
|
|
const u32 cmddata = Common::swap24(objectdata);
|
|
objectdata += 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...");
|
|
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)
|
|
{
|
|
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);
|
|
}
|