/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2023 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include "PerformanceCounterSelection.h" #include #include #include "Code/Interface/QRDInterface.h" #include "Code/Resources.h" #include "ui_PerformanceCounterSelection.h" #include #define JSON_ID "rdocPerformanceCounterSettings" #define JSON_VER 1 // we can't specialise the template, but creating an overload works. This lets us use // QSet inline uint qHash(const GPUCounter &t) { return qHash(uint32_t(t)); } namespace { enum class CounterFamily { Unknown, Generic, AMD, Intel, NVIDIA, VulkanExtended, ARM, }; CounterFamily GetCounterFamily(GPUCounter counter) { if(IsAMDCounter(counter)) { return CounterFamily::AMD; } else if(IsIntelCounter(counter)) { return CounterFamily::Intel; } else if(IsNvidiaCounter(counter)) { return CounterFamily::NVIDIA; } else if(IsVulkanExtendedCounter(counter)) { return CounterFamily::VulkanExtended; } else if(IsARMCounter(counter)) { return CounterFamily::ARM; } return CounterFamily::Generic; } QString ToString(CounterFamily family) { switch(family) { case CounterFamily::AMD: return lit("AMD"); case CounterFamily::Generic: return lit("Generic"); case CounterFamily::Intel: return lit("Intel"); case CounterFamily::NVIDIA: return lit("NVIDIA"); case CounterFamily::VulkanExtended: return lit("Vulkan Extended"); case CounterFamily::ARM: return lit("ARM"); case CounterFamily::Unknown: return lit("Unknown"); } return QString(); } } // namespace const int PerformanceCounterSelection::CounterDescriptionRole = Qt::UserRole + 1; const int PerformanceCounterSelection::CounterIdRole = Qt::UserRole + 2; const int PerformanceCounterSelection::PreviousCheckStateRole = Qt::UserRole + 3; void PerformanceCounterSelection::uncheckAllChildren(RDTreeWidgetItem *item) { for(int i = 0; i < item->childCount(); i++) { uncheckAllChildren(item->child(i)); item->child(i)->setCheckState(0, Qt::Unchecked); item->child(i)->setData(0, PreviousCheckStateRole, Qt::Unchecked); } } void PerformanceCounterSelection::checkAllChildren(RDTreeWidgetItem *item) { for(int i = 0; i < item->childCount(); i++) { checkAllChildren(item->child(i)); item->child(i)->setCheckState(0, Qt::Checked); item->child(i)->setData(0, PreviousCheckStateRole, Qt::Checked); } } void PerformanceCounterSelection::updateParentCheckState(RDTreeWidgetItem *item) { if(!item) return; int numChecked = 0; int numPartial = 0; for(int i = 0; i < item->childCount(); i++) { Qt::CheckState state = item->child(i)->checkState(0); if(state == Qt::PartiallyChecked) numPartial++; else if(state == Qt::Checked) numChecked++; } if(numChecked == item->childCount()) item->setCheckState(0, Qt::Checked); else if(numChecked > 0 || numPartial > 0) item->setCheckState(0, Qt::PartiallyChecked); else item->setCheckState(0, Qt::Unchecked); item->setData(0, PreviousCheckStateRole, item->checkState(0)); updateParentCheckState(item->parent()); } void PerformanceCounterSelection::expandToNode(RDTreeWidgetItem *node) { RDTreeWidgetItem *n = node; while(node != NULL) { ui->counterTree->expandItem(node); node = node->parent(); } if(n) ui->counterTree->scrollToItem(n); } PerformanceCounterSelection::PerformanceCounterSelection(ICaptureContext &ctx, const QList &selectedCounters, QWidget *parent) : QDialog(parent), ui(new Ui::PerformanceCounterSelection), m_Ctx(ctx) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->counterTree->setColumns({QString()}); ui->counterTree->setHeaderHidden(true); connect(ui->counterTree, &RDTreeWidget::currentItemChanged, [this](RDTreeWidgetItem *item, RDTreeWidgetItem *) -> void { const QVariant d = item->data(0, CounterDescriptionRole); if(d.isValid()) { ui->counterDescription->setText( QString(lit("%1
%2")).arg(item->text(0)).arg(d.toString())); } }); connect(ui->save, &QPushButton::clicked, this, &PerformanceCounterSelection::Save); connect(ui->load, &QPushButton::clicked, this, &PerformanceCounterSelection::Load); connect(ui->sampleCounters, &QPushButton::clicked, this, &PerformanceCounterSelection::accept); connect(ui->cancel, &QPushButton::clicked, this, &PerformanceCounterSelection::reject); connect(ui->counterTree, &RDTreeWidget::itemChanged, [this](RDTreeWidgetItem *item, int) -> void { const QVariant d = item->data(0, CounterIdRole); static bool recurse = false; if(d.isValid()) { if(item->checkState(0) == Qt::Checked) { // Add if(!m_SelectedCounters.contains((GPUCounter)d.toUInt())) { QListWidgetItem *listItem = new QListWidgetItem(ui->enabledCounters); listItem->setText(item->text(0)); listItem->setData(CounterIdRole, d); m_SelectedCounters.insert((GPUCounter)d.toUInt(), listItem); } } else { // Remove QListWidgetItem *listItem = m_SelectedCounters.take((GPUCounter)d.toUInt()); delete listItem; } if(!recurse) { recurse = true; updateParentCheckState(item->parent()); recurse = false; } } else if(!recurse) { Qt::CheckState prev = item->data(0, PreviousCheckStateRole).value(); if(item->checkState(0) != prev) { recurse = true; if(item->checkState(0) == Qt::Checked) { checkAllChildren(item); } else { uncheckAllChildren(item); } item->setData(0, PreviousCheckStateRole, item->checkState(0)); updateParentCheckState(item); recurse = false; } } }); ui->counterTree->setMouseTracking(true); QPointer ptr(this); ctx.Replay().AsyncInvoke([this, ptr, selectedCounters](IReplayController *controller) { QVector counterDescriptions; for(const GPUCounter counter : controller->EnumerateCounters()) { counterDescriptions.append(controller->DescribeCounter(counter)); } if(ptr) { GUIInvoke::call(this, [counterDescriptions, selectedCounters, this]() { SetCounters(counterDescriptions); SetSelectedCounters(selectedCounters); }); } }); } PerformanceCounterSelection::~PerformanceCounterSelection() { delete ui; } void PerformanceCounterSelection::SetCounters(const QVector &descriptions) { ui->counterTree->clear(); ui->enabledCounters->clear(); RDTreeWidgetItem *currentRoot = NULL; CounterFamily currentFamily = CounterFamily::Unknown; QMap categories; for(const CounterDescription &desc : descriptions) { m_CounterToUuid[desc.counter] = desc.uuid; m_UuidToCounter[desc.uuid] = desc.counter; const CounterFamily family = GetCounterFamily(desc.counter); if(family != currentFamily) { currentRoot = new RDTreeWidgetItem(); currentRoot->setText(0, ToString(family)); currentRoot->setCheckState(0, Qt::Unchecked); currentRoot->setData(0, PreviousCheckStateRole, Qt::Unchecked); ui->counterTree->addTopLevelItem(currentRoot); categories.clear(); currentFamily = family; } RDTreeWidgetItem *categoryItem = NULL; if(desc.category.empty()) { // Show uncategorized counters at the root level categoryItem = currentRoot; } else { const rdcstr category = desc.category; auto categoryIterator = categories.find(category); if(categoryIterator == categories.end()) { RDTreeWidgetItem *item = new RDTreeWidgetItem(); item->setText(0, desc.category); item->setCheckState(0, Qt::Unchecked); item->setData(0, PreviousCheckStateRole, Qt::Unchecked); currentRoot->addChild(item); categories[category] = item; categoryItem = item; } else { categoryItem = categoryIterator.value(); } } RDTreeWidgetItem *counterItem = new RDTreeWidgetItem(); counterItem->setText(0, desc.name); counterItem->setData(0, CounterDescriptionRole, desc.description); counterItem->setData(0, CounterIdRole, (uint32_t)desc.counter); counterItem->setCheckState(0, Qt::Unchecked); counterItem->setData(0, PreviousCheckStateRole, Qt::Unchecked); categoryItem->addChild(counterItem); m_CounterToTreeItem[desc.counter] = counterItem; } } QList PerformanceCounterSelection::GetSelectedCounters() const { return m_SelectedCounters.keys(); } void PerformanceCounterSelection::SetSelectedCounters(const QList &counters) { // We we walk over the complete tree, and toggle everything so it // matches the settings RDTreeWidgetItemIterator it(ui->counterTree); while(*it) { const QVariant id = (*it)->data(0, CounterIdRole); if(id.isValid()) { const GPUCounter counter = (GPUCounter)id.toUInt(); (*it)->setCheckState(0, counters.contains(counter) ? Qt::Checked : Qt::Unchecked); } // The loop above will uncheck all unknown counters, and not crash if some counter is no // longer present, or unknown ++it; } } void PerformanceCounterSelection::Save() { QString filename = RDDialog::getSaveFileName(this, tr("Save File"), QDir::homePath(), tr("Performance Counter Settings (*.json)")); if(filename.isEmpty()) return; QVariantList counterIds; for(const GPUCounter v : m_SelectedCounters.keys()) { const Uuid uuid = m_CounterToUuid[v]; QVariantList e; for(const uint32_t b : uuid.words) { e.append(b); } counterIds.append(QVariant(e)); } QVariantMap doc; doc[lit("counters")] = counterIds; QFile f(filename); if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { SaveToJSON(doc, f, JSON_ID, JSON_VER); } else { RDDialog::critical(this, tr("Error saving config"), tr("Couldn't open path %1 for write.").arg(filename)); } } void PerformanceCounterSelection::Load() { QString filename = RDDialog::getOpenFileName(this, tr("Load file"), QDir::homePath(), tr("Performance Counter Settings (*.json)")); if(filename.isEmpty()) return; QVariantMap doc; QFile f(filename); if(f.open(QIODevice::ReadOnly | QIODevice::Text)) { bool success = LoadFromJSON(doc, f, JSON_ID, JSON_VER); if(success) { QSet selectedCounters; QVariantList counters = doc[lit("counters")].toList(); for(const QVariant &counter : counters) { QVariantList bytes = counter.toList(); Uuid uuid; if(bytes.size() != 4) { qWarning() << "Counter ID doesn't count 4 words"; continue; } for(int i = 0; i < 4; ++i) { uuid.words[i] = bytes[i].toUInt(); } if(!m_UuidToCounter.contains(uuid)) continue; selectedCounters.insert(m_UuidToCounter[uuid]); } SetSelectedCounters(selectedCounters.toList()); } else { RDDialog::critical(this, tr("Error loading config"), tr("Couldn't interpret settings in %1.").arg(filename)); } } else { RDDialog::critical(this, tr("Error loading config"), tr("Couldn't open path %1 for reading.").arg(filename)); } } void PerformanceCounterSelection::on_enabledCounters_activated(const QModelIndex &index) { QListWidgetItem *item = ui->enabledCounters->item(index.row()); if(!item) return; QVariant counterID = item->data(CounterIdRole); // locate the item in the description tree auto it = m_CounterToTreeItem.find((GPUCounter)counterID.toUInt()); if(it != m_CounterToTreeItem.end()) { ui->counterTree->setCurrentItem(it.value()); ui->counterTree->setSelectedItem(it.value()); expandToNode(it.value()); } }