// Copyright 2018 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. #include "DolphinQt/FIFO/FIFOAnalyzer.h" #include #include #include #include #include #include #include #include #include #include #include #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 search_val; for (size_t i = 0; i < length; i++) { const QString byte_str = search_str.mid(static_cast(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(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(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); }