// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "DebugAnalysisSettingsWidget.h" #include "SettingsWindow.h" #include "SettingWidgetBinder.h" #include "DebugTools/SymbolImporter.h" #include DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(QWidget* parent) : QWidget(parent) { m_ui.setupUi(this); m_ui.automaticallyClearSymbols->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true)); setupSymbolSourceGrid(); m_ui.importFromElf->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymbolsFromELF", true)); m_ui.importSymFileFromDefaultLocation->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "ImportSymFileFromDefaultLocation", true)); m_ui.demangleSymbols->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "DemangleSymbols", true)); m_ui.demangleParameters->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "DemangleParameters", true)); setupSymbolFileList(); std::string function_scan_mode = Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanMode"); for (int i = 0;; i++) { if (Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i] == nullptr) break; if (function_scan_mode == Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames[i]) m_ui.functionScanMode->setCurrentIndex(i); } m_ui.customAddressRange->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "CustomFunctionScanRange", false)); m_ui.addressRangeStart->setText(QString::fromStdString(Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanStartAddress", ""))); m_ui.addressRangeEnd->setText(QString::fromStdString(Host::GetStringSettingValue("Debugger/Analysis", "FunctionScanEndAddress", ""))); m_ui.grayOutOverwrittenFunctions->setChecked(Host::GetBoolSettingValue("Debugger/Analysis", "GenerateFunctionHashes", true)); connect(m_ui.automaticallyClearSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); connect(m_ui.demangleSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); connect(m_ui.customAddressRange, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); updateEnabledStates(); } DebugAnalysisSettingsWidget::DebugAnalysisSettingsWidget(SettingsWindow* dialog, QWidget* parent) : QWidget(parent) , m_dialog(dialog) { SettingsInterface* sif = dialog->getSettingsInterface(); m_ui.setupUi(this); // Make sure the user doesn't select symbol sources from both the global // settings and the per-game settings, as these settings will conflict with // each other. It only really makes sense to modify these settings on a // per-game basis anyway. if (dialog->isPerGameSettings()) { SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.automaticallyClearSymbols, "Debugger/Analysis", "AutomaticallySelectSymbolsToClear", true); m_dialog->registerWidgetHelp(m_ui.automaticallyClearSymbols, tr("Automatically Select Symbols To Clear"), tr("Checked"), tr("Automatically delete symbols that were generated by any previous analysis runs.")); setupSymbolSourceGrid(); } else { m_ui.clearExistingSymbolsGroup->hide(); } SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.importFromElf, "Debugger/Analysis", "ImportSymbolsFromELF", true); SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.importSymFileFromDefaultLocation, "Debugger/Analysis", "ImportSymFileFromDefaultLocation", true); SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.demangleSymbols, "Debugger/Analysis", "DemangleSymbols", true); SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.demangleParameters, "Debugger/Analysis", "DemangleParameters", true); m_dialog->registerWidgetHelp(m_ui.importFromElf, tr("Import From ELF"), tr("Checked"), tr("Import symbol tables stored in the game's boot ELF.")); m_dialog->registerWidgetHelp(m_ui.importSymFileFromDefaultLocation, tr("Import Default .sym File"), tr("Checked"), tr("Import symbols from a .sym file with the same name as the loaded ISO file on disk if such a file exists.")); m_dialog->registerWidgetHelp(m_ui.demangleSymbols, tr("Demangle Symbols"), tr("Checked"), tr("Demangle C++ symbols during the import process so that the function and global variable names shown in the " "debugger are more readable.")); m_dialog->registerWidgetHelp(m_ui.demangleParameters, tr("Demangle Parameters"), tr("Checked"), tr("Include parameter lists in demangled function names.")); // Same as above. It only makes sense to load extra symbol files on a // per-game basis. if (dialog->isPerGameSettings()) { setupSymbolFileList(); } else { m_ui.symbolFileLabel->hide(); m_ui.symbolFileTable->hide(); m_ui.importSymbolFileButtons->hide(); } SettingWidgetBinder::BindWidgetToEnumSetting( sif, m_ui.functionScanMode, "Debugger/Analysis", "FunctionScanMode", Pcsx2Config::DebugAnalysisOptions::FunctionScanModeNames, DebugFunctionScanMode::SCAN_ELF); m_dialog->registerWidgetHelp(m_ui.functionScanMode, tr("Scan Mode"), tr("Scan ELF"), tr("Choose where the function scanner looks to find functions. This option can be useful if the application " "loads additional code at runtime.")); // Same as above. It only makes sense to set a custom memory range on a // per-game basis. if (dialog->isPerGameSettings()) { SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.customAddressRange, "Debugger/Analysis", "CustomFunctionScanRange", false); connect(m_ui.addressRangeStart, &QLineEdit::textChanged, this, &DebugAnalysisSettingsWidget::functionScanRangeChanged); connect(m_ui.addressRangeEnd, &QLineEdit::textChanged, this, &DebugAnalysisSettingsWidget::functionScanRangeChanged); m_dialog->registerWidgetHelp(m_ui.customAddressRange, tr("Custom Address Range"), tr("Unchecked"), tr("Whether to look for functions from the address range specified (Checked), or from the ELF segment " "containing the entry point (Unchecked).")); } else { m_ui.customAddressRange->hide(); m_ui.customAddressRangeLineEdits->hide(); } SettingWidgetBinder::BindWidgetToBoolSetting( sif, m_ui.grayOutOverwrittenFunctions, "Debugger/Analysis", "GenerateFunctionHashes", true); m_dialog->registerWidgetHelp(m_ui.grayOutOverwrittenFunctions, tr("Gray Out Symbols For Overwritten Functions"), tr("Checked"), tr("Generate hashes for all the detected functions, and gray out the symbols displayed in the debugger for " "functions that no longer match.")); connect(m_ui.automaticallyClearSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); connect(m_ui.demangleSymbols, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); connect(m_ui.customAddressRange, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); updateEnabledStates(); } void DebugAnalysisSettingsWidget::parseSettingsFromWidgets(Pcsx2Config::DebugAnalysisOptions& output) { output.AutomaticallySelectSymbolsToClear = m_ui.automaticallyClearSymbols->isChecked(); for (const auto& [name, temp] : m_symbol_sources) { DebugSymbolSource& source = output.SymbolSources.emplace_back(); source.Name = name; source.ClearDuringAnalysis = temp.check_box->isChecked(); } output.ImportSymbolsFromELF = m_ui.importFromElf->isChecked(); output.ImportSymFileFromDefaultLocation = m_ui.importSymFileFromDefaultLocation->isChecked(); output.DemangleSymbols = m_ui.demangleSymbols->isChecked(); output.DemangleParameters = m_ui.demangleParameters->isChecked(); for (int i = 0; i < m_symbol_file_model->rowCount(); i++) { DebugExtraSymbolFile& file = output.ExtraSymbolFiles.emplace_back(); file.Path = m_symbol_file_model->item(i, PATH_COLUMN)->text().toStdString(); file.BaseAddress = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN)->text().toStdString(); file.Condition = m_symbol_file_model->item(i, CONDITION_COLUMN)->text().toStdString(); } output.FunctionScanMode = static_cast(m_ui.functionScanMode->currentIndex()); output.CustomFunctionScanRange = m_ui.customAddressRange->isChecked(); output.FunctionScanStartAddress = m_ui.addressRangeStart->text().toStdString(); output.FunctionScanEndAddress = m_ui.addressRangeEnd->text().toStdString(); } void DebugAnalysisSettingsWidget::setupSymbolSourceGrid() { QGridLayout* layout = new QGridLayout(m_ui.symbolSourceGrid); if (!m_dialog || m_dialog->getSerial() == QtHost::GetCurrentGameSerial().toStdString()) { // Add symbol sources for which the user has already selected whether or // not they should be cleared. int existing_symbol_source_count = getIntSettingValue("Debugger/Analysis/SymbolSources", "Count", 0); for (int i = 0; i < existing_symbol_source_count; i++) { std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i); std::string name = getStringSettingValue(section.c_str(), "Name", ""); bool value = getBoolSettingValue(section.c_str(), "ClearDuringAnalysis", false); SymbolSourceTemp& source = m_symbol_sources[name]; source.previous_value = value; source.modified_by_user = true; } // Add any more symbol sources for which the user hasn't made a // selection. These are separate since we don't want to have to store // configuration data for them. R5900SymbolGuardian.Read([&](const ccc::SymbolDatabase& database) { for (const ccc::SymbolSource& symbol_source : database.symbol_sources) { if (m_symbol_sources.find(symbol_source.name()) == m_symbol_sources.end() && symbol_source.name() != "Built-In") { SymbolSourceTemp& source = m_symbol_sources[symbol_source.name()]; source.previous_value = SymbolImporter::ShouldClearSymbolsFromSourceByDefault(symbol_source.name()); source.modified_by_user = false; } } }); if (m_symbol_sources.empty()) { m_ui.symbolSourceErrorMessage->setText(tr("No symbol sources in database.")); m_ui.symbolSourceScrollArea->hide(); return; } // Create the check boxes. int i = 0; for (auto& [name, temp] : m_symbol_sources) { temp.check_box = new QCheckBox(QString::fromStdString(name)); temp.check_box->setChecked(temp.previous_value); layout->addWidget(temp.check_box, i / 2, i % 2); connect(temp.check_box, &QCheckBox::checkStateChanged, this, &DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged); i++; } } else { m_ui.symbolSourceErrorMessage->setText(tr("Start this game to modify the symbol sources list.")); m_ui.symbolSourceScrollArea->hide(); return; } m_ui.symbolSourceErrorMessage->hide(); } void DebugAnalysisSettingsWidget::symbolSourceCheckStateChanged() { QCheckBox* check_box = qobject_cast(sender()); if (!check_box) return; auto temp = m_symbol_sources.find(check_box->text().toStdString()); if (temp == m_symbol_sources.end()) return; temp->second.modified_by_user = true; saveSymbolSources(); } void DebugAnalysisSettingsWidget::saveSymbolSources() { if (!m_dialog) return; SettingsInterface* sif = m_dialog->getSettingsInterface(); if (!sif) return; // Clean up old configuration entries. int old_count = sif->GetIntValue("Debugger/Analysis/SymbolSources", "Count"); for (int i = 0; i < old_count; i++) { std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i); sif->RemoveSection(section.c_str()); } sif->RemoveSection("Debugger/Analysis/SymbolSources"); int symbol_sources_to_save = 0; for (auto& [name, temp] : m_symbol_sources) if (temp.modified_by_user) symbol_sources_to_save++; if (symbol_sources_to_save == 0) return; // Make new configuration entries. sif->SetIntValue("Debugger/Analysis/SymbolSources", "Count", symbol_sources_to_save); int i = 0; for (auto& [name, temp] : m_symbol_sources) { if (!temp.modified_by_user) continue; std::string section = "Debugger/Analysis/SymbolSources/" + std::to_string(i); sif->SetStringValue(section.c_str(), "Name", name.c_str()); sif->SetBoolValue(section.c_str(), "ClearDuringAnalysis", temp.check_box->isChecked()); i++; } } void DebugAnalysisSettingsWidget::setupSymbolFileList() { m_symbol_file_model = new QStandardItemModel(0, SYMBOL_FILE_COLUMN_COUNT, m_ui.symbolFileTable); QStringList headers; headers.emplace_back(tr("Path")); headers.emplace_back(tr("Base Address")); headers.emplace_back(tr("Condition")); m_symbol_file_model->setHorizontalHeaderLabels(headers); m_ui.symbolFileTable->setModel(m_symbol_file_model); m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(PATH_COLUMN, QHeaderView::Stretch); m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(BASE_ADDRESS_COLUMN, QHeaderView::Fixed); m_ui.symbolFileTable->horizontalHeader()->setSectionResizeMode(CONDITION_COLUMN, QHeaderView::Fixed); m_ui.symbolFileTable->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); int extra_symbol_file_count = getIntSettingValue("Debugger/Analysis/ExtraSymbolFiles", "Count", 0); for (int i = 0; i < extra_symbol_file_count; i++) { std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i); int row = m_symbol_file_model->rowCount(); if (!m_symbol_file_model->insertRow(row)) continue; QStandardItem* path_item = new QStandardItem(); path_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Path", ""))); m_symbol_file_model->setItem(row, PATH_COLUMN, path_item); QStandardItem* base_address_item = new QStandardItem(); base_address_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "BaseAddress"))); m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item); QStandardItem* condition_item = new QStandardItem(); condition_item->setText(QString::fromStdString(getStringSettingValue(section.c_str(), "Condition"))); m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item); } connect(m_ui.addSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::addSymbolFile); connect(m_ui.removeSymbolFile, &QPushButton::clicked, this, &DebugAnalysisSettingsWidget::removeSymbolFile); connect(m_ui.symbolFileTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); connect(m_symbol_file_model, &QStandardItemModel::dataChanged, this, &DebugAnalysisSettingsWidget::saveSymbolFiles); connect(m_symbol_file_model, &QStandardItemModel::dataChanged, this, &DebugAnalysisSettingsWidget::updateEnabledStates); } void DebugAnalysisSettingsWidget::addSymbolFile() { std::string path = Path::ToNativePath(QFileDialog::getOpenFileName(this, tr("Add Symbol File")).toStdString()); if (path.empty()) return; std::string relative_path = Path::MakeRelative(path, EmuFolders::GameSettings); if (!relative_path.starts_with("..")) path = std::move(relative_path); int row = m_symbol_file_model->rowCount(); if (!m_symbol_file_model->insertRow(row)) return; QStandardItem* path_item = new QStandardItem(); path_item->setText(QString::fromStdString(path)); m_symbol_file_model->setItem(row, PATH_COLUMN, path_item); QStandardItem* base_address_item = new QStandardItem(); base_address_item->setText(""); m_symbol_file_model->setItem(row, BASE_ADDRESS_COLUMN, base_address_item); QStandardItem* condition_item = new QStandardItem(); condition_item->setText(""); m_symbol_file_model->setItem(row, CONDITION_COLUMN, condition_item); saveSymbolFiles(); updateEnabledStates(); } void DebugAnalysisSettingsWidget::removeSymbolFile() { QItemSelectionModel* selection_model = m_ui.symbolFileTable->selectionModel(); if (!selection_model) return; while (!selection_model->selectedIndexes().isEmpty()) { QModelIndex index = selection_model->selectedIndexes().first(); m_symbol_file_model->removeRow(index.row(), index.parent()); } saveSymbolFiles(); updateEnabledStates(); } void DebugAnalysisSettingsWidget::saveSymbolFiles() { if (!m_dialog) return; SettingsInterface* sif = m_dialog->getSettingsInterface(); if (!sif) return; // Clean up old configuration entries. int old_count = sif->GetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count"); for (int i = 0; i < old_count; i++) { std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i); sif->RemoveSection(section.c_str()); } sif->RemoveSection("Debugger/Analysis/ExtraSymbolFiles"); if (m_symbol_file_model->rowCount() == 0) return; // Make new configuration entries. sif->SetIntValue("Debugger/Analysis/ExtraSymbolFiles", "Count", m_symbol_file_model->rowCount()); for (int i = 0; i < m_symbol_file_model->rowCount(); i++) { std::string section = "Debugger/Analysis/ExtraSymbolFiles/" + std::to_string(i); if (QStandardItem* path_item = m_symbol_file_model->item(i, PATH_COLUMN)) sif->SetStringValue(section.c_str(), "Path", path_item->text().toStdString().c_str()); if (QStandardItem* base_address_item = m_symbol_file_model->item(i, BASE_ADDRESS_COLUMN)) sif->SetStringValue(section.c_str(), "BaseAddress", base_address_item->text().toStdString().c_str()); if (QStandardItem* condition_item = m_symbol_file_model->item(i, CONDITION_COLUMN)) sif->SetStringValue(section.c_str(), "Condition", condition_item->text().toStdString().c_str()); } QtHost::SaveGameSettings(sif, true); g_emu_thread->reloadGameSettings(); } void DebugAnalysisSettingsWidget::functionScanRangeChanged() { if (!m_dialog) return; SettingsInterface* sif = m_dialog->getSettingsInterface(); if (!sif) return; QString start_address = m_ui.addressRangeStart->text(); QString end_address = m_ui.addressRangeEnd->text(); bool ok; if (start_address.toUInt(&ok, 16), ok) sif->SetStringValue("Debugger/Analysis", "FunctionScanStartAddress", start_address.toStdString().c_str()); if (end_address.toUInt(&ok, 16), ok) sif->SetStringValue("Debugger/Analysis", "FunctionScanEndAddress", end_address.toStdString().c_str()); } void DebugAnalysisSettingsWidget::updateEnabledStates() { m_ui.symbolSourceScrollArea->setEnabled(!m_ui.automaticallyClearSymbols->isChecked()); m_ui.symbolSourceErrorMessage->setEnabled(!m_ui.automaticallyClearSymbols->isChecked()); m_ui.demangleParameters->setEnabled(m_ui.demangleSymbols->isChecked()); m_ui.removeSymbolFile->setEnabled( m_ui.symbolFileTable->selectionModel() && m_ui.symbolFileTable->selectionModel()->hasSelection()); m_ui.customAddressRangeLineEdits->setEnabled(m_ui.customAddressRange->isChecked()); } std::string DebugAnalysisSettingsWidget::getStringSettingValue( const char* section, const char* key, const char* default_value) { if (m_dialog) return m_dialog->getEffectiveStringValue(section, key, default_value); return Host::GetStringSettingValue(section, key, default_value); } bool DebugAnalysisSettingsWidget::getBoolSettingValue( const char* section, const char* key, bool default_value) { if (m_dialog) return m_dialog->getEffectiveBoolValue(section, key, default_value); return Host::GetBoolSettingValue(section, key, default_value); } int DebugAnalysisSettingsWidget::getIntSettingValue( const char* section, const char* key, int default_value) { if (m_dialog) return m_dialog->getEffectiveIntValue(section, key, default_value); return Host::GetIntSettingValue(section, key, default_value); }