mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-01 12:32:48 +00:00
d5cfac71d0
This also adds the commands after the last primitive data but before the next frame as a unique object; this is mainly just the XFB copy. It's nice to have these visible, though disabling the object does nothing since only primitive data is disabled and there is no primitive data in this case.
744 lines
23 KiB
C++
744 lines
23 KiB
C++
// Copyright 2018 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "DolphinQt/FIFO/FIFOAnalyzer.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#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/VertexLoaderBase.h"
|
|
#include "VideoCommon/XFStructs.h"
|
|
|
|
// Values range from 0 to number of frames - 1
|
|
constexpr int FRAME_ROLE = Qt::UserRole;
|
|
// Values range from 0 to number of parts - 1
|
|
constexpr int PART_START_ROLE = Qt::UserRole + 1;
|
|
// Values range from 1 to number of parts
|
|
constexpr int PART_END_ROLE = Qt::UserRole + 2;
|
|
|
|
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 AnalyzedFrameInfo& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame);
|
|
ASSERT(frame_info.parts.size() != 0);
|
|
|
|
Common::EnumMap<u32, FramePartType::PrimitiveData> part_counts;
|
|
u32 part_start = 0;
|
|
|
|
for (u32 part_nr = 0; part_nr < frame_info.parts.size(); part_nr++)
|
|
{
|
|
const auto& part = frame_info.parts[part_nr];
|
|
|
|
const u32 part_type_nr = part_counts[part.m_type];
|
|
part_counts[part.m_type]++;
|
|
|
|
QTreeWidgetItem* object_item = nullptr;
|
|
if (part.m_type == FramePartType::PrimitiveData)
|
|
object_item = new QTreeWidgetItem({tr("Object %1").arg(part_type_nr)});
|
|
// We don't create dedicated labels for FramePartType::Command;
|
|
// those are grouped with the primitive
|
|
|
|
if (object_item != nullptr)
|
|
{
|
|
frame_item->addChild(object_item);
|
|
|
|
object_item->setData(0, FRAME_ROLE, frame);
|
|
object_item->setData(0, PART_START_ROLE, part_start);
|
|
object_item->setData(0, PART_END_ROLE, part_nr);
|
|
|
|
part_start = part_nr + 1;
|
|
}
|
|
}
|
|
|
|
// Final data (the XFB copy)
|
|
if (part_start != frame_info.parts.size())
|
|
{
|
|
QTreeWidgetItem* object_item = new QTreeWidgetItem({tr("Final Data")});
|
|
frame_item->addChild(object_item);
|
|
|
|
object_item->setData(0, FRAME_ROLE, frame);
|
|
object_item->setData(0, PART_START_ROLE, part_start);
|
|
object_item->setData(0, PART_END_ROLE, u32(frame_info.parts.size() - 1));
|
|
}
|
|
|
|
// The counts we computed should match the frame's counts
|
|
ASSERT(std::equal(frame_info.part_type_counts.begin(), frame_info.part_type_counts.end(),
|
|
part_counts.begin()));
|
|
}
|
|
}
|
|
|
|
static std::string GetPrimitiveName(u8 cmd)
|
|
{
|
|
if ((cmd & 0xC0) != 0x80)
|
|
{
|
|
PanicAlertFmt("Not a primitive command: {:#04x}", cmd);
|
|
return "";
|
|
}
|
|
const u8 vat = cmd & OpcodeDecoder::GX_VAT_MASK; // Vertex loader index (0 - 7)
|
|
const u8 primitive =
|
|
(cmd & OpcodeDecoder::GX_PRIMITIVE_MASK) >> OpcodeDecoder::GX_PRIMITIVE_SHIFT;
|
|
return fmt::format("{} VAT {}", static_cast<OpcodeDecoder::Primitive>(primitive), vat);
|
|
}
|
|
|
|
void FIFOAnalyzer::UpdateDetails()
|
|
{
|
|
using OpcodeDecoder::Opcode;
|
|
|
|
// 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, PART_START_ROLE).isNull())
|
|
return;
|
|
|
|
const u32 frame_nr = items[0]->data(0, FRAME_ROLE).toUInt();
|
|
const u32 start_part_nr = items[0]->data(0, PART_START_ROLE).toUInt();
|
|
const u32 end_part_nr = items[0]->data(0, PART_END_ROLE).toUInt();
|
|
|
|
const AnalyzedFrameInfo& frame_info = FifoPlayer::GetInstance().GetAnalyzedFrameInfo(frame_nr);
|
|
const auto& fifo_frame = FifoPlayer::GetInstance().GetFile()->GetFrame(frame_nr);
|
|
|
|
const u32 object_start = frame_info.parts[start_part_nr].m_start;
|
|
const u32 object_end = frame_info.parts[end_part_nr].m_end;
|
|
const u32 object_size = object_end - object_start;
|
|
|
|
const u8* const object = &fifo_frame.fifoData[object_start];
|
|
|
|
u32 object_offset = 0;
|
|
while (object_offset < object_size)
|
|
{
|
|
QString new_label;
|
|
const u32 start_offset = object_offset;
|
|
m_object_data_offsets.push_back(start_offset);
|
|
|
|
const Opcode opcode = static_cast<Opcode>(object[object_offset++]);
|
|
switch (opcode)
|
|
{
|
|
case Opcode::GX_NOP:
|
|
if (object[object_offset] == static_cast<u8>(Opcode::GX_NOP))
|
|
{
|
|
u32 nop_count = 2;
|
|
while (object[++object_offset] == static_cast<u8>(Opcode::GX_NOP))
|
|
nop_count++;
|
|
|
|
new_label = QStringLiteral("NOP (%1x)").arg(nop_count);
|
|
}
|
|
else
|
|
{
|
|
new_label = QStringLiteral("NOP");
|
|
}
|
|
break;
|
|
|
|
case Opcode::GX_CMD_UNKNOWN_METRICS:
|
|
new_label = QStringLiteral("GX_CMD_UNKNOWN_METRICS");
|
|
break;
|
|
|
|
case Opcode::GX_CMD_INVL_VC:
|
|
new_label = QStringLiteral("GX_CMD_INVL_VC");
|
|
break;
|
|
|
|
case Opcode::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 Opcode::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 Opcode::GX_LOAD_INDX_A:
|
|
{
|
|
const auto [desc, written] =
|
|
GetXFIndexedLoadInfo(CPArray::XF_A, Common::swap32(&object[object_offset]));
|
|
object_offset += 4;
|
|
new_label = QStringLiteral("LOAD INDX A %1").arg(QString::fromStdString(desc));
|
|
}
|
|
break;
|
|
case Opcode::GX_LOAD_INDX_B:
|
|
{
|
|
const auto [desc, written] =
|
|
GetXFIndexedLoadInfo(CPArray::XF_B, Common::swap32(&object[object_offset]));
|
|
object_offset += 4;
|
|
new_label = QStringLiteral("LOAD INDX B %1").arg(QString::fromStdString(desc));
|
|
}
|
|
break;
|
|
case Opcode::GX_LOAD_INDX_C:
|
|
{
|
|
const auto [desc, written] =
|
|
GetXFIndexedLoadInfo(CPArray::XF_C, Common::swap32(&object[object_offset]));
|
|
object_offset += 4;
|
|
new_label = QStringLiteral("LOAD INDX C %1").arg(QString::fromStdString(desc));
|
|
}
|
|
break;
|
|
case Opcode::GX_LOAD_INDX_D:
|
|
{
|
|
const auto [desc, written] =
|
|
GetXFIndexedLoadInfo(CPArray::XF_D, Common::swap32(&object[object_offset]));
|
|
object_offset += 4;
|
|
new_label = QStringLiteral("LOAD INDX D %1").arg(QString::fromStdString(desc));
|
|
}
|
|
break;
|
|
|
|
case Opcode::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 Opcode::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:
|
|
{
|
|
const u8 command = static_cast<u8>(opcode);
|
|
if ((command & 0xC0) == 0x80)
|
|
{
|
|
// Object primitive data
|
|
const u8 vat = command & OpcodeDecoder::GX_VAT_MASK;
|
|
const auto& vtx_desc = frame_info.parts[end_part_nr].m_cpmem.vtxDesc;
|
|
const auto& vtx_attr = frame_info.parts[end_part_nr].m_cpmem.vtxAttr[vat];
|
|
|
|
const auto name = GetPrimitiveName(command);
|
|
|
|
const u16 vertex_count = Common::swap16(&object[object_offset]);
|
|
object_offset += 2;
|
|
const u32 vertex_size = VertexLoaderBase::GetVertexSize(vtx_desc, vtx_attr);
|
|
|
|
// Note that vertex_count is allowed to be 0, with no special treatment
|
|
// (another command just comes right after the current command, with no vertices in between)
|
|
const u32 object_prim_size = vertex_count * vertex_size;
|
|
|
|
new_label = QStringLiteral("PRIMITIVE %1 (%2) %3 vertices %4 bytes/vertex %5 total bytes")
|
|
.arg(QString::fromStdString(name))
|
|
.arg(command, 2, 16, QLatin1Char('0'))
|
|
.arg(vertex_count)
|
|
.arg(vertex_size)
|
|
.arg(object_prim_size);
|
|
|
|
// It's not really useful to have a massive unreadable hex string for the object primitives.
|
|
// Put it in the description instead.
|
|
|
|
// #define INCLUDE_HEX_IN_PRIMITIVES
|
|
#ifdef INCLUDE_HEX_IN_PRIMITIVES
|
|
new_label += QStringLiteral(" ");
|
|
for (u32 i = 0; i < object_prim_size; i++)
|
|
{
|
|
new_label += QStringLiteral("%1").arg(object[object_offset++], 2, 16, QLatin1Char('0'));
|
|
}
|
|
#else
|
|
object_offset += object_prim_size;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
new_label = QStringLiteral("Unknown opcode %1").arg(command, 2, 16);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
new_label = QStringLiteral("%1: ").arg(object_start + start_offset, 8, 16, QLatin1Char('0')) +
|
|
new_label;
|
|
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, PART_START_ROLE).isNull())
|
|
{
|
|
m_search_label->setText(tr("Invalid search parameters (no object selected)"));
|
|
return;
|
|
}
|
|
|
|
// Having PART_START_ROLE indicates that this is valid
|
|
const int object_idx = items[0]->parent()->indexOfChild(items[0]);
|
|
|
|
// 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 start_part_nr = items[0]->data(0, PART_START_ROLE).toUInt();
|
|
const u32 end_part_nr = items[0]->data(0, PART_END_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 = frame_info.parts[start_part_nr].m_start;
|
|
const u32 object_end = frame_info.parts[end_part_nr].m_end;
|
|
const u32 object_size = object_end - 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_idx, 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_idx);
|
|
|
|
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()
|
|
{
|
|
using OpcodeDecoder::Opcode;
|
|
|
|
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, PART_START_ROLE).isNull())
|
|
return;
|
|
|
|
const u32 frame_nr = items[0]->data(0, FRAME_ROLE).toUInt();
|
|
const u32 start_part_nr = items[0]->data(0, PART_START_ROLE).toUInt();
|
|
const u32 end_part_nr = items[0]->data(0, PART_END_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 = frame_info.parts[start_part_nr].m_start;
|
|
const u32 entry_start = m_object_data_offsets[entry_nr];
|
|
|
|
const u8* cmddata = &fifo_frame.fifoData[object_start + entry_start];
|
|
const Opcode opcode = static_cast<Opcode>(*cmddata);
|
|
|
|
// TODO: Not sure whether we should bother translating the descriptions
|
|
|
|
QString text;
|
|
if (opcode == Opcode::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 (opcode == Opcode::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 (opcode == Opcode::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 if (opcode == Opcode::GX_LOAD_INDX_A)
|
|
{
|
|
const auto [desc, written] = GetXFIndexedLoadInfo(CPArray::XF_A, Common::swap32(cmddata + 1));
|
|
|
|
text = QString::fromStdString(desc);
|
|
text += QLatin1Char{'\n'};
|
|
text += tr("Usually used for position matrices");
|
|
text += QLatin1Char{'\n'};
|
|
text += QString::fromStdString(written);
|
|
}
|
|
else if (opcode == Opcode::GX_LOAD_INDX_B)
|
|
{
|
|
const auto [desc, written] = GetXFIndexedLoadInfo(CPArray::XF_B, Common::swap32(cmddata + 1));
|
|
|
|
text = QString::fromStdString(desc);
|
|
text += QLatin1Char{'\n'};
|
|
// i18n: A normal matrix is a matrix used for transforming normal vectors. The word "normal"
|
|
// does not have its usual meaning here, but rather the meaning of "perpendicular to a surface".
|
|
text += tr("Usually used for normal matrices");
|
|
text += QLatin1Char{'\n'};
|
|
text += QString::fromStdString(written);
|
|
}
|
|
else if (opcode == Opcode::GX_LOAD_INDX_C)
|
|
{
|
|
const auto [desc, written] = GetXFIndexedLoadInfo(CPArray::XF_C, Common::swap32(cmddata + 1));
|
|
|
|
text = QString::fromStdString(desc);
|
|
text += QLatin1Char{'\n'};
|
|
// i18n: Tex coord is short for texture coordinate
|
|
text += tr("Usually used for tex coord matrices");
|
|
text += QLatin1Char{'\n'};
|
|
text += QString::fromStdString(written);
|
|
}
|
|
else if (opcode == Opcode::GX_LOAD_INDX_D)
|
|
{
|
|
const auto [desc, written] = GetXFIndexedLoadInfo(CPArray::XF_D, Common::swap32(cmddata + 1));
|
|
|
|
text = QString::fromStdString(desc);
|
|
text += QLatin1Char{'\n'};
|
|
text += tr("Usually used for light objects");
|
|
text += QLatin1Char{'\n'};
|
|
text += QString::fromStdString(written);
|
|
}
|
|
else if ((*cmddata & 0xC0) == 0x80)
|
|
{
|
|
const u8 vat = *cmddata & OpcodeDecoder::GX_VAT_MASK;
|
|
const QString name = QString::fromStdString(GetPrimitiveName(*cmddata));
|
|
const u16 vertex_count = Common::swap16(cmddata + 1);
|
|
|
|
// i18n: In this context, a primitive means a point, line, triangle or rectangle.
|
|
// Do not translate the word primitive as if it was an adjective.
|
|
text = tr("Primitive %1").arg(name);
|
|
text += QLatin1Char{'\n'};
|
|
|
|
const auto& vtx_desc = frame_info.parts[end_part_nr].m_cpmem.vtxDesc;
|
|
const auto& vtx_attr = frame_info.parts[end_part_nr].m_cpmem.vtxAttr[vat];
|
|
const auto component_sizes = VertexLoaderBase::GetVertexComponentSizes(vtx_desc, vtx_attr);
|
|
|
|
u32 i = 3;
|
|
for (u32 vertex_num = 0; vertex_num < vertex_count; vertex_num++)
|
|
{
|
|
text += QLatin1Char{'\n'};
|
|
for (u32 comp_size : component_sizes)
|
|
{
|
|
for (u32 comp_off = 0; comp_off < comp_size; comp_off++)
|
|
{
|
|
text += QStringLiteral("%1").arg(cmddata[i++], 2, 16, QLatin1Char('0'));
|
|
}
|
|
text += QLatin1Char{' '};
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
text = tr("No description available");
|
|
}
|
|
|
|
m_entry_detail_browser->setText(text);
|
|
}
|