// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Debugger/ShaderHunterWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/FileUtil.h" #include "Common/CommonTypes.h" #include "Core/Config/GraphicsSettings.h" #include "Core/ConfigManager.h" #include "DolphinQt/Settings.h" #include "VideoCommon/ShaderHunter.h" namespace { static QString ToHashHex(u64 hash) { return QStringLiteral("%1").arg(static_cast(hash), 16, 16, QLatin1Char('0')); } static std::unordered_map FindTexturePreviewPaths(const std::vector& hashes) { std::unordered_map result; if (hashes.empty()) return result; std::unordered_set remaining(hashes.begin(), hashes.end()); std::unordered_map patterns; patterns.reserve(remaining.size()); for (u64 hash : remaining) patterns.emplace(hash, QStringLiteral("_%1_").arg(ToHashHex(hash))); const QString dump_root = QString::fromStdString(File::GetUserPath(D_DUMPTEXTURES_IDX)); QStringList roots; const std::string game_id = SConfig::GetInstance().GetGameID(); if (!game_id.empty()) roots.push_back(QDir::cleanPath(dump_root + QString::fromStdString(game_id))); roots.push_back(QDir::cleanPath(dump_root)); for (const QString& root : roots) { if (!QDir(root).exists()) continue; QDirIterator it(root, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext() && !remaining.empty()) { const QString path = it.next(); const QFileInfo info(path); const QString suffix = info.suffix().toLower(); if (suffix != QStringLiteral("png") && suffix != QStringLiteral("jpg") && suffix != QStringLiteral("jpeg") && suffix != QStringLiteral("bmp") && suffix != QStringLiteral("tga")) { continue; } const QString filename = info.fileName().toLower(); for (auto iter = remaining.begin(); iter != remaining.end();) { const u64 hash = *iter; if (filename.contains(patterns[hash])) { result.emplace(hash, path); iter = remaining.erase(iter); } else { ++iter; } } } if (remaining.empty()) break; } return result; } static QPixmap LoadPreviewPixmapFresh(const QString& path) { QImageReader reader(path); reader.setAutoTransform(true); const QImage image = reader.read(); if (image.isNull()) return {}; return QPixmap::fromImage(image); } } // namespace ShaderHunterWidget::ShaderHunterWidget(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Shader Hunter")); setWindowFlags(windowFlags() | Qt::Window); setAttribute(Qt::WA_DeleteOnClose); setMinimumWidth(300); CreateWidgets(); ConnectSignals(); m_update_timer = new QTimer(this); connect(m_update_timer, &QTimer::timeout, this, &ShaderHunterWidget::UpdateDisplay); m_update_timer->start(100); } void ShaderHunterWidget::CreateWidgets() { auto* layout = new QVBoxLayout; m_main_layout = layout; m_enable_checkbox = new QCheckBox(tr("Enable Shader Hunting")); layout->addWidget(m_enable_checkbox); auto* hunting_option_layout = new QHBoxLayout; hunting_option_layout->addWidget(new QLabel(tr("Hunting Option:"))); m_hunting_option_combo = new QComboBox; m_hunting_option_combo->addItem(tr("Skip"), static_cast(ShaderHunter::HuntingOption::Skip)); m_hunting_option_combo->addItem(tr("Pink"), static_cast(ShaderHunter::HuntingOption::Pink)); hunting_option_layout->addWidget(m_hunting_option_combo); layout->addLayout(hunting_option_layout); auto* match_mode_layout = new QHBoxLayout; match_mode_layout->addWidget(new QLabel(tr("Match Type:"))); m_match_mode_combo = new QComboBox; m_match_mode_combo->addItem(tr("Shader Family"), static_cast(ShaderHunter::MatchMode::ShaderFamily)); m_match_mode_combo->addItem(tr("Exact Hash"), static_cast(ShaderHunter::MatchMode::ExactHash)); m_match_mode_combo->setToolTip( tr("Controls both the live Skip/Pink preview and the saved override.\n" "Shader Family matches semantic shader variants and is more resilient to BaijiSteamVR " "shader-generator updates.")); match_mode_layout->addWidget(m_match_mode_combo); layout->addLayout(match_mode_layout); auto* type_layout = new QHBoxLayout; type_layout->addWidget(new QLabel(tr("Shader Type:"))); m_type_combo = new QComboBox; m_type_combo->addItem(tr("Pixel Shader")); m_type_combo->addItem(tr("Vertex Shader")); m_type_combo->addItem(tr("Geometry Shader")); type_layout->addWidget(m_type_combo); layout->addLayout(type_layout); m_hash_label = new QLabel(tr("Hash: (none)")); m_hash_label->setFont(QFont(QStringLiteral("Courier"))); layout->addWidget(m_hash_label); m_family_signature_label = new QLabel(tr("Family: (none)")); m_family_signature_label->setFont(QFont(QStringLiteral("Courier"))); layout->addWidget(m_family_signature_label); m_position_label = new QLabel(tr("- / -")); layout->addWidget(m_position_label); auto* nav_layout = new QHBoxLayout; m_prev_button = new QPushButton(tr("<< Previous")); m_next_button = new QPushButton(tr("Next >>")); nav_layout->addWidget(m_prev_button); nav_layout->addWidget(m_next_button); layout->addLayout(nav_layout); // Save shader override section m_shader_name_edit = new QLineEdit; m_shader_name_edit->setPlaceholderText(tr("Enter shader name...")); layout->addWidget(m_shader_name_edit); auto* handling_layout = new QHBoxLayout; handling_layout->addWidget(new QLabel(tr("Override Handling:"))); m_handling_combo = new QComboBox; m_handling_combo->addItem(tr("Skip Draw"), static_cast(ShaderHunter::HandlingType::Skip)); m_handling_combo->addItem(tr("Screen"), static_cast(ShaderHunter::HandlingType::Screen)); m_handling_combo->addItem(tr("Fullscreen"), static_cast(ShaderHunter::HandlingType::Fullscreen)); m_handling_combo->addItem(tr("Head Locked"), static_cast(ShaderHunter::HandlingType::HeadLocked)); m_handling_combo->addItem(tr("Flag"), static_cast(ShaderHunter::HandlingType::Flag)); m_handling_combo->addItem(tr("Units per Meter"), static_cast(ShaderHunter::HandlingType::UnitsPerMeter)); m_handling_combo->addItem(tr("Passthrough"), static_cast(ShaderHunter::HandlingType::Passthrough)); handling_layout->addWidget(m_handling_combo); layout->addLayout(handling_layout); auto* upm_layout = new QHBoxLayout; m_units_per_meter_label = new QLabel(tr("Units per Meter:")); m_units_per_meter_spin = new QDoubleSpinBox; m_units_per_meter_spin->setRange(Config::GFX_VR_UNITS_PER_METER_MIN, Config::GFX_VR_UNITS_PER_METER_MAX); m_units_per_meter_spin->setDecimals(2); m_units_per_meter_spin->setSingleStep(Config::GFX_VR_UNITS_PER_METER_STEP); m_units_per_meter_spin->setValue(1.0); m_units_per_meter_spin->setToolTip( tr("Temporary per-shader scale override for VR when handling is Units per Meter.")); upm_layout->addWidget(m_units_per_meter_label); upm_layout->addWidget(m_units_per_meter_spin); layout->addLayout(upm_layout); m_save_button = new QPushButton(tr("Save Shader")); layout->addWidget(m_save_button); m_dump_button = new QPushButton(tr("Dump Shader")); layout->addWidget(m_dump_button); m_textures_button = new QPushButton(tr("View Textures")); m_textures_button->setToolTip( tr("Show texture hashes used by the currently selected shader.\n" "You can toggle texture-based skipping to isolate specific textures.")); layout->addWidget(m_textures_button); auto* texture_mode_layout = new QHBoxLayout; texture_mode_layout->addWidget(new QLabel(tr("Texture Filter Mode:"))); m_texture_filter_mode_combo = new QComboBox; m_texture_filter_mode_combo->addItem(tr("Include"), false); m_texture_filter_mode_combo->addItem(tr("Exclude"), true); m_texture_filter_mode_combo->setToolTip( tr("Include: apply override only when selected textures are present.\n" "Exclude: apply override only when selected textures are absent.")); texture_mode_layout->addWidget(m_texture_filter_mode_combo); layout->addLayout(texture_mode_layout); m_selected_texture_label = new QLabel; m_selected_texture_label->setFont(QFont(QStringLiteral("Courier"))); m_selected_texture_label->setVisible(false); layout->addWidget(m_selected_texture_label); setLayout(m_main_layout); } void ShaderHunterWidget::ConnectSignals() { connect(m_enable_checkbox, &QCheckBox::toggled, this, [this](bool checked) { ShaderHunter::GetInstance().SetEnabled(checked); m_hunting_option_combo->setEnabled(checked); }); connect(m_hunting_option_combo, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { auto& hunter = ShaderHunter::GetInstance(); const auto option = static_cast( m_hunting_option_combo->currentData().toInt()); hunter.SetHuntingOption(option); }); connect(m_match_mode_combo, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { m_saved_texture_filters.clear(); SetSelectedTextureHashes({}); auto& hunter = ShaderHunter::GetInstance(); hunter.SetHuntingMatchMode(static_cast( m_match_mode_combo->currentData().toInt())); UpdateDisplay(); }); connect(m_type_combo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { m_saved_texture_filters.clear(); SetSelectedTextureHashes({}); ShaderHunter::GetInstance().SetActiveType(static_cast(index)); }); connect(m_prev_button, &QPushButton::clicked, this, [this] { m_saved_texture_filters.clear(); SetSelectedTextureHashes({}); ShaderHunter::GetInstance().PrevShader(); UpdateDisplay(); }); connect(m_next_button, &QPushButton::clicked, this, [this] { m_saved_texture_filters.clear(); SetSelectedTextureHashes({}); ShaderHunter::GetInstance().NextShader(); UpdateDisplay(); }); connect(m_save_button, &QPushButton::clicked, this, &ShaderHunterWidget::SaveCurrentShader); connect(m_dump_button, &QPushButton::clicked, this, &ShaderHunterWidget::DumpCurrentShader); connect(m_textures_button, &QPushButton::clicked, this, &ShaderHunterWidget::ShowTexturesDialog); connect(m_texture_filter_mode_combo, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { SetSelectedTextureHashes(m_saved_texture_filters); }); connect(m_handling_combo, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { const auto handling = static_cast(m_handling_combo->currentData().toInt()); const bool show_upm = handling == ShaderHunter::HandlingType::UnitsPerMeter; m_units_per_meter_label->setVisible(show_upm); m_units_per_meter_spin->setVisible(show_upm); }); const auto handling = static_cast(m_handling_combo->currentData().toInt()); const bool show_upm = handling == ShaderHunter::HandlingType::UnitsPerMeter; m_units_per_meter_label->setVisible(show_upm); m_units_per_meter_spin->setVisible(show_upm); const auto hunting_option = ShaderHunter::GetInstance().GetHuntingOption(); const int hunting_idx = m_hunting_option_combo->findData(static_cast(hunting_option)); if (hunting_idx >= 0) m_hunting_option_combo->setCurrentIndex(hunting_idx); const auto match_mode = ShaderHunter::GetInstance().GetHuntingMatchMode(); const int match_mode_idx = m_match_mode_combo->findData(static_cast(match_mode)); if (match_mode_idx >= 0) m_match_mode_combo->setCurrentIndex(match_mode_idx); m_hunting_option_combo->setEnabled(ShaderHunter::GetInstance().IsEnabled()); } void ShaderHunterWidget::UpdateDisplay() { auto& hunter = ShaderHunter::GetInstance(); const int pos = hunter.GetSelectedPosition(); const int total = hunter.GetTotalCount(); const u64 hash = hunter.GetSelectedHash(); const auto family_signature = hunter.GetShaderFamilySignature(hunter.GetActiveType(), hash); if (pos >= 0 && total > 0) { m_hash_label->setText( tr("Hash: 0x%1").arg(static_cast(hash), 8, 16, QLatin1Char('0')).toUpper()); m_position_label->setText(tr("%1 / %2").arg(pos + 1).arg(total)); if (family_signature.has_value()) { m_family_signature_label->setText( tr("Family: 0x%1") .arg(static_cast(*family_signature), 16, 16, QLatin1Char('0')) .toUpper()); } else { m_family_signature_label->setText(tr("Family: (unavailable)")); } } else { m_hash_label->setText(tr("Hash: (none)")); m_family_signature_label->setText(tr("Family: (none)")); m_position_label->setText(tr("- / %1").arg(total)); } } void ShaderHunterWidget::SaveCurrentShader() { auto& hunter = ShaderHunter::GetInstance(); const int pos = hunter.GetSelectedPosition(); if (pos < 0) { QMessageBox::warning(this, tr("Save Shader"), tr("No shader selected. Use hunting to select a shader first.")); return; } const std::string game_id = SConfig::GetInstance().GetGameID(); if (game_id.empty()) { QMessageBox::warning(this, tr("Save Shader"), tr("No game is currently running.")); return; } // Load existing overrides first so we can auto-generate a unique name when empty. auto all = ShaderHunter::LoadOverridesFromINI(game_id); std::string name = m_shader_name_edit->text().toStdString(); if (name.empty()) { constexpr std::string_view unnamed_prefix = "Unnamed Shader"; int max_unnamed_index = 0; for (const auto& existing : all) { const std::string& existing_name = existing.name; if (existing_name == unnamed_prefix) { max_unnamed_index = std::max(max_unnamed_index, 1); continue; } if (!existing_name.starts_with(unnamed_prefix) || existing_name.size() <= unnamed_prefix.size()) continue; if (existing_name[unnamed_prefix.size()] != ' ') continue; const std::string suffix = existing_name.substr(unnamed_prefix.size() + 1); if (suffix.empty() || !std::all_of(suffix.begin(), suffix.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) { continue; } max_unnamed_index = std::max(max_unnamed_index, std::stoi(suffix)); } name = fmt::format("Unnamed Shader {}", max_unnamed_index + 1); } u64 hash = hunter.GetSelectedHash(); auto type = hunter.GetActiveType(); const auto match_mode = static_cast( m_match_mode_combo->currentData().toInt()); const auto family_signature = hunter.GetShaderFamilySignature(type, hash); if (match_mode == ShaderHunter::MatchMode::ShaderFamily && !family_signature.has_value()) { QMessageBox::warning( this, tr("Save Shader"), tr("The selected shader family signature is not available yet. Let the shader render " "again, then retry. The override was not saved as an exact hash because that would " "not survive shader-generator updates.")); return; } const auto handling = static_cast(m_handling_combo->currentData().toInt()); ShaderHunter::ShaderOverride entry; entry.name = name; entry.hash = hash; entry.type = type; entry.handling = handling; entry.match_mode = match_mode; entry.hash_family_match = match_mode == ShaderHunter::MatchMode::ShaderFamily; if (family_signature.has_value()) { entry.family_signature = *family_signature; entry.family_version = ShaderHunter::FAMILY_SCHEME_VERSION; } entry.enabled = true; entry.user_defined = true; if (handling == ShaderHunter::HandlingType::UnitsPerMeter) entry.units_per_meter = static_cast(m_units_per_meter_spin->value()); // Quick-save defaults Passthrough to fully see-through; adjust via the edit dialog. if (handling == ShaderHunter::HandlingType::Passthrough) entry.passthrough_opacity = 0.0f; if (handling == ShaderHunter::HandlingType::Flag) entry.flag_group = name; // Include texture filters saved from the Texture Hunter popup (if any). entry.texture_hashes.insert(entry.texture_hashes.end(), m_saved_texture_filters.begin(), m_saved_texture_filters.end()); std::sort(entry.texture_hashes.begin(), entry.texture_hashes.end()); entry.texture_hashes.erase( std::unique(entry.texture_hashes.begin(), entry.texture_hashes.end()), entry.texture_hashes.end()); entry.texture_hashes_excluded = !entry.texture_hashes.empty() && m_texture_filter_mode_combo->currentData().toBool(); // Append and save all.push_back(entry); ShaderHunter::SaveOverridesToINI(game_id, all); // Reload runtime overrides hunter.LoadOverrides(game_id); emit OverridesChanged(); Settings::Instance().NotifyShaderOverridesChanged(); const char* type_str = type == ShaderHunter::ShaderType::Pixel ? "PS" : type == ShaderHunter::ShaderType::Vertex ? "VS" : "GS"; const char* handling_str = handling == ShaderHunter::HandlingType::Screen ? "screen" : handling == ShaderHunter::HandlingType::Fullscreen || handling == ShaderHunter::HandlingType::FullscreenMono ? "fullscreen" : handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" : handling == ShaderHunter::HandlingType::Flag ? "flag" : handling == ShaderHunter::HandlingType::UnitsPerMeter ? "units_per_meter" : handling == ShaderHunter::HandlingType::Passthrough ? "passthrough" : "skip"; const QString match_mode_text = entry.hash_family_match ? tr("Shader Family") : tr("Exact Hash"); QString msg = tr("Saved shader override '%1' (%2, hash 0x%3, match: %4, handling: %5)") .arg(QString::fromStdString(name)) .arg(QString::fromLatin1(type_str)) .arg(static_cast(hash), 8, 16, QLatin1Char('0')) .arg(match_mode_text) .arg(QString::fromLatin1(handling_str)); if (entry.family_signature != 0) { msg += tr("\nFamily signature: 0x%1 (scheme v%2)") .arg(static_cast(entry.family_signature), 16, 16, QLatin1Char('0')) .arg(entry.family_version); } if (handling == ShaderHunter::HandlingType::UnitsPerMeter && entry.units_per_meter > 0.0f) msg += tr("\nUnits per Meter: %1").arg(entry.units_per_meter, 0, 'f', 2); if (!entry.texture_hashes.empty()) { QStringList hash_list; for (u64 texture_hash : entry.texture_hashes) hash_list.append(QString::number(texture_hash, 16).rightJustified(16, QLatin1Char('0'))); const QString mode_text = entry.texture_hashes_excluded ? tr("Exclude") : tr("Include"); msg += tr("\nTexture filter mode: %1").arg(mode_text); msg += tr("\nTexture filter(s): %1").arg(hash_list.join(QStringLiteral(", "))); } msg += tr("\nSaved to %1.ini").arg(QString::fromStdString(game_id)); QMessageBox::information(this, tr("Save Shader"), msg); m_shader_name_edit->clear(); m_saved_texture_filters.clear(); m_texture_filter_mode_combo->setCurrentIndex(0); SetSelectedTextureHashes({}); } void ShaderHunterWidget::closeEvent(QCloseEvent* event) { auto& hunter = ShaderHunter::GetInstance(); hunter.SetEnabled(false); m_saved_texture_filters.clear(); m_texture_filter_mode_combo->setCurrentIndex(0); SetSelectedTextureHashes({}); QDialog::closeEvent(event); } void ShaderHunterWidget::DumpCurrentShader() { auto& hunter = ShaderHunter::GetInstance(); const int pos = hunter.GetSelectedPosition(); if (pos < 0) { QMessageBox::warning(this, tr("Dump Shader"), tr("No shader selected. Use hunting to select a shader first.")); return; } const std::string game_id = SConfig::GetInstance().GetGameID(); if (game_id.empty()) { QMessageBox::warning(this, tr("Dump Shader"), tr("No game is currently running.")); return; } const u64 hash = hunter.GetSelectedHash(); const auto type = hunter.GetActiveType(); if (hunter.DumpShader(game_id, type, hash)) { const char* type_suffix = type == ShaderHunter::ShaderType::Pixel ? "ps" : type == ShaderHunter::ShaderType::Vertex ? "vs" : "gs"; QMessageBox::information( this, tr("Dump Shader"), tr("Dumped shader to Dump/Shaders/%1/%2-%3.txt") .arg(QString::fromStdString(game_id)) .arg(static_cast(hash), 8, 16, QLatin1Char('0')) .arg(QString::fromLatin1(type_suffix))); } else { QMessageBox::warning(this, tr("Dump Shader"), tr("Failed to dump shader. The shader UID may not be cached yet.\n" "Make sure shader hunting is enabled and the shader has been " "seen in the current session.")); } } void ShaderHunterWidget::ShowTexturesDialog() { auto& hunter = ShaderHunter::GetInstance(); const int pos = hunter.GetSelectedPosition(); if (pos < 0) { QMessageBox::warning(this, tr("View Textures"), tr("No shader selected. Enable hunting and select a shader first.")); return; } const u64 hash = hunter.GetSelectedHash(); const auto type = hunter.GetActiveType(); const char* type_str = type == ShaderHunter::ShaderType::Pixel ? "PS" : type == ShaderHunter::ShaderType::Vertex ? "VS" : "GS"; hunter.SetTextureToolActive(true); auto* dlg = new QDialog(this); dlg->setWindowTitle(tr("Textures for %1 %2") .arg(QString::fromLatin1(type_str)) .arg(static_cast(hash), 8, 16, QLatin1Char('0'))); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setMinimumSize(620, 320); auto* tree = new QTreeWidget; tree->setHeaderLabels({tr("Skip"), tr("Texture Hash"), tr("Name"), tr("Preview")}); tree->setRootIsDecorated(false); tree->setSelectionMode(QAbstractItemView::ExtendedSelection); tree->header()->setStretchLastSection(true); tree->setIconSize(QSize(64, 64)); auto* info = new QLabel; info->setWordWrap(true); auto* continuous_scan_check = new QCheckBox(tr("Continuous Scan")); continuous_scan_check->setToolTip( tr("Continuously refresh the list while this window is open.\n" "Textures seen at least once during scan stay visible in gray.")); auto* scan_timer = new QTimer(dlg); scan_timer->setInterval(250); auto seen_textures = std::make_shared>(); connect(dlg, &QObject::destroyed, this, []() { ShaderHunter::GetInstance().SetTextureToolActive(false); }); const auto update_selected_label = [this, tree]() { std::vector selected_hashes; selected_hashes.reserve(tree->topLevelItemCount()); for (int i = 0; i < tree->topLevelItemCount(); i++) { QTreeWidgetItem* item = tree->topLevelItem(i); if (!item || item->checkState(0) != Qt::Checked) continue; const u64 texture_hash = item->data(0, Qt::UserRole).toULongLong(); if (texture_hash != 0) selected_hashes.push_back(texture_hash); } std::sort(selected_hashes.begin(), selected_hashes.end()); selected_hashes.erase(std::unique(selected_hashes.begin(), selected_hashes.end()), selected_hashes.end()); SetSelectedTextureHashes(std::vector(selected_hashes.begin(), selected_hashes.end())); }; const auto populate = [tree, info, continuous_scan_check, seen_textures, &hunter, type_str, hash]() { const auto textures = hunter.GetTexturesForSelectedShader(); std::unordered_map current_textures; current_textures.reserve(textures.size()); for (const auto& tex : textures) current_textures.emplace(tex.hash, tex.name); if (continuous_scan_check->isChecked()) { for (const auto& tex : textures) { auto& saved_name = (*seen_textures)[tex.hash]; if (saved_name.empty() && !tex.name.empty()) saved_name = tex.name; } } std::vector texture_hashes; texture_hashes.reserve(current_textures.size() + seen_textures->size()); for (const auto& [texture_hash, _] : current_textures) texture_hashes.push_back(texture_hash); for (const auto& [texture_hash, _] : *seen_textures) texture_hashes.push_back(texture_hash); std::sort(texture_hashes.begin(), texture_hashes.end()); texture_hashes.erase(std::unique(texture_hashes.begin(), texture_hashes.end()), texture_hashes.end()); const auto preview_paths = FindTexturePreviewPaths(texture_hashes); const QSignalBlocker blocker(tree); tree->clear(); int seen_only_count = 0; for (u64 texture_hash : texture_hashes) { const auto current_it = current_textures.find(texture_hash); const bool in_current_frame = current_it != current_textures.end(); if (!in_current_frame) seen_only_count++; auto* item = new QTreeWidgetItem; item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); item->setCheckState(0, hunter.IsTextureSkipEnabled(texture_hash) ? Qt::Checked : Qt::Unchecked); item->setText(1, QStringLiteral("%1").arg(static_cast(texture_hash), 16, 16, QLatin1Char('0'))); if (in_current_frame) { const std::string& name = current_it->second; item->setText(2, name.empty() ? QObject::tr("(unknown)") : QString::fromStdString(name)); } else { const auto seen_it = seen_textures->find(texture_hash); const bool has_seen_name = seen_it != seen_textures->end() && !seen_it->second.empty(); item->setText(2, has_seen_name ? QString::fromStdString(seen_it->second) : QObject::tr("(seen during scan, not in current frame)")); const QBrush gray_brush(QColor(140, 140, 140)); item->setForeground(1, gray_brush); item->setForeground(2, gray_brush); } item->setData(0, Qt::UserRole, static_cast(texture_hash)); auto preview_it = preview_paths.find(texture_hash); if (preview_it != preview_paths.end()) { QPixmap pixmap = LoadPreviewPixmapFresh(preview_it->second); if (!pixmap.isNull()) { item->setData(3, Qt::DecorationRole, pixmap.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); item->setSizeHint(3, QSize(72, 72)); } } tree->addTopLevelItem(item); } for (int i = 0; i < 4; i++) tree->resizeColumnToContents(i); if (current_textures.empty() && seen_only_count == 0) { info->setText(QObject::tr( "No textures captured yet for %1 %2.\n" "Enable hunting and let at least one frame render with this shader selected.\n" "Then use Refresh or Continuous Scan.") .arg(QString::fromLatin1(type_str)) .arg(static_cast(hash), 8, 16, QLatin1Char('0'))); } else if (current_textures.empty()) { info->setText(QObject::tr( "No textures in current frame for %1 %2.\n" "%3 texture hash(es) were seen during scan and are shown in gray.") .arg(QString::fromLatin1(type_str)) .arg(static_cast(hash), 8, 16, QLatin1Char('0')) .arg(seen_only_count)); } else { info->setText(QObject::tr( "%1 texture hash(es) in current frame for %3 %4. %2 seen earlier (gray).\n" "Check a row to skip draws using that texture for the selected shader.") .arg(current_textures.size()) .arg(seen_only_count) .arg(QString::fromLatin1(type_str)) .arg(static_cast(hash), 8, 16, QLatin1Char('0'))); } }; connect(tree, &QTreeWidget::itemChanged, dlg, [&hunter, update_selected_label](QTreeWidgetItem* item, int column) { if (!item || column != 0) return; const u64 texture_hash = item->data(0, Qt::UserRole).toULongLong(); hunter.SetTextureSkipEnabled(texture_hash, item->checkState(0) == Qt::Checked); update_selected_label(); }); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close); auto* refresh_button = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole); auto* clear_button = buttons->addButton(tr("Clear Checked"), QDialogButtonBox::ActionRole); auto* save_button = buttons->addButton(tr("Save To Shader"), QDialogButtonBox::ActionRole); connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::close); connect(refresh_button, &QPushButton::clicked, dlg, [populate, update_selected_label]() { populate(); update_selected_label(); }); connect(scan_timer, &QTimer::timeout, dlg, [populate, update_selected_label]() { populate(); update_selected_label(); }); connect(continuous_scan_check, &QCheckBox::toggled, dlg, [scan_timer, populate, update_selected_label](bool checked) { if (checked) { populate(); update_selected_label(); scan_timer->start(); } else { scan_timer->stop(); } }); connect(clear_button, &QPushButton::clicked, dlg, [&hunter, populate, update_selected_label]() { hunter.ClearTextureSkipFilters(); populate(); update_selected_label(); }); connect(save_button, &QPushButton::clicked, dlg, [this, tree, dlg]() { std::vector selected_hashes; selected_hashes.reserve(tree->topLevelItemCount()); for (int i = 0; i < tree->topLevelItemCount(); i++) { QTreeWidgetItem* item = tree->topLevelItem(i); if (!item || item->checkState(0) != Qt::Checked) continue; const u64 texture_hash = item->data(0, Qt::UserRole).toULongLong(); if (texture_hash != 0) selected_hashes.push_back(texture_hash); } std::sort(selected_hashes.begin(), selected_hashes.end()); selected_hashes.erase(std::unique(selected_hashes.begin(), selected_hashes.end()), selected_hashes.end()); m_saved_texture_filters = std::move(selected_hashes); SetSelectedTextureHashes( std::vector(m_saved_texture_filters.begin(), m_saved_texture_filters.end())); QMessageBox::information( dlg, tr("Texture Filters Saved"), tr("Saved %1 texture hash(es) to the current shader.\n" "Press 'Save Shader' in Shader Hunter to write them to the config.") .arg(m_saved_texture_filters.size())); }); auto* layout = new QVBoxLayout; layout->addWidget(info); layout->addWidget(tree); auto* bottom_layout = new QHBoxLayout; bottom_layout->addWidget(continuous_scan_check); bottom_layout->addStretch(); bottom_layout->addWidget(buttons); layout->addLayout(bottom_layout); dlg->setLayout(layout); populate(); update_selected_label(); QTimer::singleShot(120, dlg, [populate, update_selected_label]() { populate(); update_selected_label(); }); dlg->show(); } void ShaderHunterWidget::SetSelectedTextureHashes(const std::vector& hashes) { if (hashes.empty()) { m_selected_texture_label->setVisible(false); return; } QStringList hash_list; for (uint64_t hash : hashes) { hash_list.append( QStringLiteral("%1").arg(static_cast(hash), 16, 16, QLatin1Char('0'))); } m_selected_texture_label->setVisible(true); const bool exclude_mode = m_texture_filter_mode_combo->currentData().toBool(); m_selected_texture_label->setText( tr("Selected Texture %1:\n%2") .arg(exclude_mode ? tr("Exclude Filter(s)") : tr("Include Filter(s)")) .arg(hash_list.join(QStringLiteral("\n")))); }