Files

1377 lines
51 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/ShaderHunterWidget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QBrush>
#include <QColor>
#include <QDir>
#include <QDirIterator>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QImageReader>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPixmap>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSize>
#include <QStringList>
#include <QTimer>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <algorithm>
#include <cctype>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <fmt/format.h>
#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<qulonglong>(hash), 16, 16, QLatin1Char('0'));
}
static std::unordered_map<u64, QString> FindTexturePreviewPaths(const std::vector<u64>& hashes)
{
std::unordered_map<u64, QString> result;
if (hashes.empty())
return result;
std::unordered_set<u64> remaining(hashes.begin(), hashes.end());
std::unordered_map<u64, QString> 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<int>(ShaderHunter::HuntingOption::Skip));
m_hunting_option_combo->addItem(tr("Pink"),
static_cast<int>(ShaderHunter::HuntingOption::Pink));
hunting_option_layout->addWidget(m_hunting_option_combo);
layout->addLayout(hunting_option_layout);
m_debug_log_checkbox = new QCheckBox(tr("Debug Log Draws"));
m_debug_log_checkbox->setToolTip(
tr("Log every draw call with projection, viewport, scissor, and shader hashes.\n"
"Also logs EFB clear operations. Use to identify how visual elements are drawn.\n"
"Open Log Configuration and enable INFO for Video to see the output.\n"
"WARNING: generates a LOT of log output — enable briefly, then disable."));
layout->addWidget(m_debug_log_checkbox);
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_group_hunt_checkbox = new QCheckBox(tr("Enable Group Hunt"));
m_group_hunt_checkbox->setToolTip(
tr("Cycle frame draws that match the selected runtime signature groups from the seeded draw."));
layout->addWidget(m_group_hunt_checkbox);
auto* group_seed_layout = new QHBoxLayout;
m_group_hunt_seed_button = new QPushButton(tr("Seed From Current Draw"));
m_group_hunt_seed_button->setToolTip(
tr("Use the currently highlighted draw as the group-hunt seed.\n"
"Group hunting then matches other draws that share the checked groups."));
group_seed_layout->addWidget(m_group_hunt_seed_button);
group_seed_layout->addStretch(1);
layout->addLayout(group_seed_layout);
auto* group_mask_layout = new QHBoxLayout;
m_group_projection_check = new QCheckBox(tr("Projection"));
m_group_layer_check = new QCheckBox(tr("Layer"));
m_group_viewport_check = new QCheckBox(tr("Viewport"));
m_group_scissor_check = new QCheckBox(tr("Scissor"));
m_group_render_state_check = new QCheckBox(tr("Render State"));
for (QCheckBox* checkbox : {m_group_projection_check, m_group_layer_check,
m_group_viewport_check, m_group_scissor_check,
m_group_render_state_check})
{
group_mask_layout->addWidget(checkbox);
}
group_mask_layout->addStretch(1);
layout->addLayout(group_mask_layout);
m_group_hunt_status_label = new QLabel(tr("Group Hunt: (no seed)"));
m_group_hunt_status_label->setWordWrap(true);
layout->addWidget(m_group_hunt_status_label);
m_hash_label = new QLabel(tr("Hash: (none)"));
m_hash_label->setFont(QFont(QStringLiteral("Courier")));
layout->addWidget(m_hash_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);
m_draw_calls_button = new QPushButton(tr("Draw Call Filter"));
m_draw_calls_button->setToolTip(
tr("Open the draw-call filter tool for the selected shader.\n"
"Use it to isolate draw calls and define a range for saved overrides."));
layout->addWidget(m_draw_calls_button);
m_selected_draw_call_label = new QLabel;
m_selected_draw_call_label->setFont(QFont(QStringLiteral("Courier")));
m_selected_draw_call_label->setVisible(false);
layout->addWidget(m_selected_draw_call_label);
// 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<int>(ShaderHunter::HandlingType::Skip));
m_handling_combo->addItem(tr("Screen"), static_cast<int>(ShaderHunter::HandlingType::Screen));
m_handling_combo->addItem(tr("Fullscreen"),
static_cast<int>(ShaderHunter::HandlingType::Fullscreen));
m_handling_combo->addItem(tr("Fullscreen Mono"),
static_cast<int>(ShaderHunter::HandlingType::FullscreenMono));
m_handling_combo->addItem(tr("Head Locked"),
static_cast<int>(ShaderHunter::HandlingType::HeadLocked));
m_handling_combo->addItem(tr("Flag"), static_cast<int>(ShaderHunter::HandlingType::Flag));
m_handling_combo->addItem(tr("Units per Meter"),
static_cast<int>(ShaderHunter::HandlingType::UnitsPerMeter));
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<int>(&QComboBox::currentIndexChanged), this,
[this](int) {
auto& hunter = ShaderHunter::GetInstance();
const auto option = static_cast<ShaderHunter::HuntingOption>(
m_hunting_option_combo->currentData().toInt());
hunter.SetHuntingOption(option);
});
connect(m_debug_log_checkbox, &QCheckBox::toggled, this, [](bool checked) {
ShaderHunter::GetInstance().SetDebugLogging(checked);
});
connect(m_type_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
SetSelectedDrawCallRange(-1, -1, 0);
m_saved_texture_filters.clear();
SetSelectedTextureHashes({});
ShaderHunter::GetInstance().SetActiveType(static_cast<ShaderHunter::ShaderType>(index));
UpdateGroupHuntUi();
});
connect(m_prev_button, &QPushButton::clicked, this, [this] {
SetSelectedDrawCallRange(-1, -1, 0);
m_saved_texture_filters.clear();
SetSelectedTextureHashes({});
ShaderHunter::GetInstance().PrevShader();
UpdateDisplay();
});
connect(m_next_button, &QPushButton::clicked, this, [this] {
SetSelectedDrawCallRange(-1, -1, 0);
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_draw_calls_button, &QPushButton::clicked, this, &ShaderHunterWidget::ShowDrawCallDialog);
connect(m_texture_filter_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int) { SetSelectedTextureHashes(m_saved_texture_filters); });
connect(m_group_hunt_checkbox, &QCheckBox::toggled, this, [this](bool checked) {
auto& hunter = ShaderHunter::GetInstance();
hunter.SetGroupHuntEnabled(checked);
UpdateGroupHuntUi();
UpdateDisplay();
});
connect(m_group_hunt_seed_button, &QPushButton::clicked, this, [this]() {
auto& hunter = ShaderHunter::GetInstance();
if (!hunter.SeedGroupHuntFromCurrentDraw())
{
QMessageBox::warning(
this, tr("Group Hunt"),
tr("No current draw is available to seed group hunt.\n"
"Use Shader Hunter or Draw Call Filter to isolate a draw first."));
return;
}
const auto signature = hunter.GetGroupHuntSeedSignature();
{
const QSignalBlocker projection_blocker(m_group_projection_check);
const QSignalBlocker layer_blocker(m_group_layer_check);
const QSignalBlocker viewport_blocker(m_group_viewport_check);
const QSignalBlocker scissor_blocker(m_group_scissor_check);
const QSignalBlocker render_state_blocker(m_group_render_state_check);
m_group_projection_check->setChecked(signature.use_projection);
m_group_layer_check->setChecked(signature.use_layer);
m_group_viewport_check->setChecked(signature.use_viewport);
m_group_scissor_check->setChecked(signature.use_scissor);
m_group_render_state_check->setChecked(signature.use_render_state);
}
UpdateGroupHuntUi();
UpdateDisplay();
});
for (QCheckBox* checkbox : {m_group_projection_check, m_group_layer_check,
m_group_viewport_check, m_group_scissor_check,
m_group_render_state_check})
{
connect(checkbox, &QCheckBox::toggled, this, [this]() {
auto& hunter = ShaderHunter::GetInstance();
hunter.SetGroupHuntGroupMask(m_group_projection_check->isChecked(),
m_group_layer_check->isChecked(),
m_group_viewport_check->isChecked(),
m_group_scissor_check->isChecked(),
m_group_render_state_check->isChecked());
UpdateGroupHuntUi();
UpdateDisplay();
});
}
connect(m_handling_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int) {
const auto handling =
static_cast<ShaderHunter::HandlingType>(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<ShaderHunter::HandlingType>(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<int>(hunting_option));
if (hunting_idx >= 0)
m_hunting_option_combo->setCurrentIndex(hunting_idx);
m_hunting_option_combo->setEnabled(ShaderHunter::GetInstance().IsEnabled());
UpdateGroupHuntUi();
}
void ShaderHunterWidget::UpdateDisplay()
{
auto& hunter = ShaderHunter::GetInstance();
const auto status = hunter.GetHuntingStatusForOSD();
UpdateGroupHuntUi();
if (status.group_hunt_enabled && status.group_hunt_seed_valid &&
(status.group_hunt_seed_signature.use_projection || status.group_hunt_seed_signature.use_layer ||
status.group_hunt_seed_signature.use_viewport || status.group_hunt_seed_signature.use_scissor ||
status.group_hunt_seed_signature.use_render_state))
{
if (status.group_hunt_current_hash != 0)
{
m_hash_label->setText(
tr("Hash: 0x%1")
.arg(static_cast<uint>(status.group_hunt_current_hash), 8, 16, QLatin1Char('0'))
.toUpper());
}
else
{
m_hash_label->setText(tr("Hash: (no group match)"));
}
if (status.group_hunt_total_matches > 0)
{
m_position_label->setText(
tr("%1 / %2")
.arg(status.group_hunt_selected_match + 1)
.arg(status.group_hunt_total_matches));
m_selected_draw_call_label->setVisible(true);
if (status.group_hunt_draw_index >= 0)
{
m_selected_draw_call_label->setText(
tr("Group Match Draw:\n#%1").arg(status.group_hunt_draw_index + 1));
}
else
{
m_selected_draw_call_label->setText(tr("Group Match Draw:\n(not seen this frame)"));
}
}
else
{
m_position_label->setText(tr("- / 0"));
m_selected_draw_call_label->setVisible(true);
m_selected_draw_call_label->setText(tr("Group Match Draw:\n(no matches)"));
}
return;
}
const int pos = hunter.GetSelectedPosition();
const int total = hunter.GetTotalCount();
const u64 hash = hunter.GetSelectedHash();
if (pos >= 0 && total > 0)
{
m_hash_label->setText(
tr("Hash: 0x%1").arg(static_cast<uint>(hash), 8, 16, QLatin1Char('0')).toUpper());
m_position_label->setText(tr("%1 / %2").arg(pos + 1).arg(total));
}
else
{
m_hash_label->setText(tr("Hash: (none)"));
m_position_label->setText(tr("- / %1").arg(total));
}
}
void ShaderHunterWidget::UpdateGroupHuntUi()
{
auto& hunter = ShaderHunter::GetInstance();
const auto status = hunter.GetHuntingStatusForOSD();
const bool enabled = status.group_hunt_enabled;
const bool has_seed = status.group_hunt_seed_valid;
const bool has_groups = has_seed &&
(status.group_hunt_seed_signature.use_projection ||
status.group_hunt_seed_signature.use_layer ||
status.group_hunt_seed_signature.use_viewport ||
status.group_hunt_seed_signature.use_scissor ||
status.group_hunt_seed_signature.use_render_state);
{
const QSignalBlocker blocker(m_group_hunt_checkbox);
m_group_hunt_checkbox->setChecked(enabled);
}
const auto seed_signature = hunter.GetGroupHuntSeedSignature();
{
const QSignalBlocker projection_blocker(m_group_projection_check);
const QSignalBlocker layer_blocker(m_group_layer_check);
const QSignalBlocker viewport_blocker(m_group_viewport_check);
const QSignalBlocker scissor_blocker(m_group_scissor_check);
const QSignalBlocker render_state_blocker(m_group_render_state_check);
m_group_projection_check->setChecked(seed_signature.use_projection);
m_group_layer_check->setChecked(seed_signature.use_layer);
m_group_viewport_check->setChecked(seed_signature.use_viewport);
m_group_scissor_check->setChecked(seed_signature.use_scissor);
m_group_render_state_check->setChecked(seed_signature.use_render_state);
}
for (QCheckBox* checkbox : {m_group_projection_check, m_group_layer_check,
m_group_viewport_check, m_group_scissor_check,
m_group_render_state_check})
{
checkbox->setEnabled(has_seed);
}
m_group_hunt_seed_button->setEnabled(hunter.IsEnabled());
m_type_combo->setEnabled(!enabled);
const char* seed_type = status.group_hunt_seed_type == ShaderHunter::ShaderType::Pixel ? "PS" :
status.group_hunt_seed_type == ShaderHunter::ShaderType::Vertex ? "VS" :
"GS";
if (!has_seed)
{
m_group_hunt_status_label->setText(tr("Group Hunt: (no seed)"));
}
else if (!has_groups)
{
m_group_hunt_status_label->setText(
tr("Group Hunt Seed: %1 0x%2\nSelect at least one signature group.")
.arg(QString::fromLatin1(seed_type))
.arg(static_cast<uint>(status.group_hunt_seed_hash), 8, 16, QLatin1Char('0')));
}
else
{
m_group_hunt_status_label->setText(
tr("Group Hunt Seed: %1 0x%2\nMatches: %3 / %4")
.arg(QString::fromLatin1(seed_type))
.arg(static_cast<uint>(status.group_hunt_seed_hash), 8, 16, QLatin1Char('0'))
.arg(status.group_hunt_total_matches > 0 ? status.group_hunt_selected_match + 1 : 0)
.arg(status.group_hunt_total_matches));
}
}
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 hunting_status = hunter.GetHuntingStatusForOSD();
const bool group_hunt_ready =
hunting_status.group_hunt_enabled && hunting_status.group_hunt_seed_valid &&
(hunting_status.group_hunt_seed_signature.use_projection ||
hunting_status.group_hunt_seed_signature.use_layer ||
hunting_status.group_hunt_seed_signature.use_viewport ||
hunting_status.group_hunt_seed_signature.use_scissor ||
hunting_status.group_hunt_seed_signature.use_render_state);
const auto hunted_draw = hunter.GetGroupHuntHighlightedDraw();
if (group_hunt_ready && !hunted_draw.has_value())
{
QMessageBox::warning(this, tr("Save Shader"),
tr("Group Hunt is active, but no matching draw is available in the current frame."));
return;
}
if (hunted_draw.has_value())
{
hash = hunted_draw->hash;
type = hunted_draw->type;
}
const auto handling =
static_cast<ShaderHunter::HandlingType>(m_handling_combo->currentData().toInt());
// Build override with element range if set
ShaderHunter::ShaderOverride entry;
entry.name = name;
entry.hash = hash;
entry.type = type;
entry.handling = handling;
entry.enabled = true;
entry.user_defined = true;
if (hunted_draw.has_value())
{
entry.match_mode = ShaderHunter::MatchMode::RuntimeElement;
entry.runtime_element = hunted_draw->signature;
if (type == ShaderHunter::ShaderType::Pixel)
{
if (const auto family_signature = hunter.GetShaderFamilySignature(type, hash);
family_signature.has_value())
{
entry.hash_family_match = true;
entry.family_signature = *family_signature;
}
}
}
else if (const auto runtime_signature = hunter.GetSelectedRuntimeElementSignature();
runtime_signature)
{
entry.match_mode = ShaderHunter::MatchMode::RuntimeElement;
entry.runtime_element = *runtime_signature;
if (type == ShaderHunter::ShaderType::Pixel)
{
if (const auto family_signature = hunter.GetShaderFamilySignature(type, hash);
family_signature.has_value())
{
entry.hash_family_match = true;
entry.family_signature = *family_signature;
}
}
}
if (handling == ShaderHunter::HandlingType::UnitsPerMeter)
entry.units_per_meter = static_cast<float>(m_units_per_meter_spin->value());
if (handling == ShaderHunter::HandlingType::Flag)
entry.flag_group = name;
// Include draw-call range saved from Draw Call Filter popup (if any),
// otherwise use the current hunting range.
if (m_saved_element_start >= 0 && m_saved_element_end >= 0)
{
entry.element_start = m_saved_element_start;
entry.element_end = m_saved_element_end;
entry.element_reference_total = m_saved_element_total;
}
else if (hunter.IsElementMode())
{
const int range_start = hunter.GetElementStart();
const int range_end = hunter.GetElementEnd();
if (range_start >= 0 && range_end >= 0)
{
entry.element_start = range_start;
entry.element_end = range_end;
entry.element_reference_total = hunter.GetElementTotal();
}
// Auto-capture texture hash from the current element cursor
const int elem = hunter.GetSelectedElement();
if (elem >= 0)
{
const auto textures = hunter.GetElementTextures(elem);
// Use the first non-zero texture hash (stage 0 is typically the main texture)
for (u64 th : textures)
{
if (th != 0)
{
entry.texture_hashes.push_back(th);
break;
}
}
}
}
else if (m_saved_element_start >= 0 && m_saved_element_end >= 0 && hunter.IsElementMode())
{
// Preserve auto texture capture when range was saved from the popup.
const int elem = hunter.GetSelectedElement();
if (elem >= 0)
{
const auto textures = hunter.GetElementTextures(elem);
for (u64 th : textures)
{
if (th != 0)
{
entry.texture_hashes.push_back(th);
break;
}
}
}
}
if (hunted_draw.has_value())
{
for (u64 th : hunted_draw->textures)
{
if (th != 0)
{
entry.texture_hashes.push_back(th);
break;
}
}
}
// 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 ? "fullscreen" :
handling == ShaderHunter::HandlingType::FullscreenMono ?
"fullscreen_mono" :
handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" :
handling == ShaderHunter::HandlingType::Flag ? "flag" :
handling == ShaderHunter::HandlingType::UnitsPerMeter ?
"units_per_meter" :
"skip";
QString msg = tr("Saved shader override '%1' (%2, hash 0x%3, handling: %4)")
.arg(QString::fromStdString(name))
.arg(QString::fromLatin1(type_str))
.arg(static_cast<uint>(hash), 8, 16, QLatin1Char('0'))
.arg(QString::fromLatin1(handling_str));
if (entry.element_start >= 0 && entry.element_end >= 0)
msg += tr("\nElement range: %1 - %2").arg(entry.element_start + 1).arg(entry.element_end + 1);
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();
SetSelectedDrawCallRange(-1, -1, 0);
m_saved_texture_filters.clear();
m_texture_filter_mode_combo->setCurrentIndex(0);
SetSelectedTextureHashes({});
}
void ShaderHunterWidget::closeEvent(QCloseEvent* event)
{
auto& hunter = ShaderHunter::GetInstance();
hunter.SetElementMode(false);
hunter.SetGroupHuntEnabled(false);
hunter.SetEnabled(false);
SetSelectedDrawCallRange(-1, -1, 0);
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<uint>(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::ShowDrawCallDialog()
{
auto& hunter = ShaderHunter::GetInstance();
const int pos = hunter.GetSelectedPosition();
if (pos < 0)
{
QMessageBox::warning(this, tr("Draw Call Filter"),
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";
auto* dlg = new QDialog(this);
dlg->setWindowTitle(tr("Draw Call Filter for %1 %2")
.arg(QString::fromLatin1(type_str))
.arg(static_cast<uint>(hash), 8, 16, QLatin1Char('0')));
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->setMinimumSize(560, 320);
auto* info_label = new QLabel(
tr("Use draw-call filtering to isolate one draw at a time or define a start/end range.\n"
"Saved overrides will include the current draw-call range."));
info_label->setWordWrap(true);
auto* enable_check = new QCheckBox(tr("Enable Draw Call Filter"));
enable_check->setChecked(hunter.IsElementMode());
auto* draw_call_label = new QLabel(tr("Draw Call: - / -"));
auto* range_label = new QLabel(tr("Range: (not set)"));
auto* textures_label = new QLabel(tr("Textures: (none)"));
textures_label->setFont(QFont(QStringLiteral("Courier"), 8));
textures_label->setWordWrap(true);
auto* prev_button = new QPushButton(tr("<< Prev Element"));
auto* next_button = new QPushButton(tr("Next Element >>"));
auto* mark_start_button = new QPushButton(tr("Mark Start"));
auto* mark_end_button = new QPushButton(tr("Mark End"));
auto* clear_range_button = new QPushButton(tr("Clear Range"));
mark_start_button->setToolTip(tr("Set the range start to the current element cursor position."));
mark_end_button->setToolTip(tr("Set the range end to the current element cursor position."));
clear_range_button->setToolTip(tr("Clear the range and go back to single-element skipping."));
auto* nav_layout = new QHBoxLayout;
nav_layout->addWidget(prev_button);
nav_layout->addWidget(next_button);
auto* range_buttons_layout = new QHBoxLayout;
range_buttons_layout->addWidget(mark_start_button);
range_buttons_layout->addWidget(mark_end_button);
range_buttons_layout->addWidget(clear_range_button);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
auto* save_button = buttons->addButton(tr("Save To Shader"), QDialogButtonBox::ActionRole);
connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::close);
const auto update_ui = [enable_check, draw_call_label, range_label, textures_label, prev_button,
next_button, mark_start_button, mark_end_button, clear_range_button,
&hunter]() {
const bool active = hunter.IsElementMode();
{
const QSignalBlocker blocker(enable_check);
enable_check->setChecked(active);
}
prev_button->setEnabled(active);
next_button->setEnabled(active);
mark_start_button->setEnabled(active);
mark_end_button->setEnabled(active);
clear_range_button->setEnabled(active);
if (!active)
{
draw_call_label->setText(QObject::tr("Draw Call: (disabled)"));
range_label->setText(QObject::tr("Range: (disabled)"));
textures_label->setText(QObject::tr("Textures: (disabled)"));
return;
}
const int elem = hunter.GetSelectedElement();
const int elem_total = hunter.GetElementTotal();
const int range_start = hunter.GetElementStart();
const int range_end = hunter.GetElementEnd();
if (elem_total > 0)
draw_call_label->setText(QObject::tr("Draw Call: %1 / %2").arg(elem + 1).arg(elem_total));
else
draw_call_label->setText(QObject::tr("Draw Call: - / 0"));
if (range_start >= 0 && range_end >= 0)
{
const int count = range_end - range_start + 1;
range_label->setText(QObject::tr("Range: %1 - %2 (%3 draw(s) skipped)")
.arg(range_start + 1)
.arg(range_end + 1)
.arg(count));
}
else
{
range_label->setText(QObject::tr("Range: (not set - skipping single element)"));
}
if (elem >= 0 && elem_total > 0)
{
const auto tex_names = hunter.GetElementTextureNames(elem);
const auto tex_hashes = hunter.GetElementTextures(elem);
QStringList tex_list;
for (int i = 0; i < 8; i++)
{
if (tex_hashes[i] == 0)
continue;
const QString name = QString::fromStdString(tex_names[i]);
if (!name.isEmpty())
{
tex_list.append(QStringLiteral("[%1] %2").arg(i).arg(name));
}
else
{
tex_list.append(QObject::tr("[%1] %2")
.arg(i)
.arg(QString::number(tex_hashes[i], 16)
.rightJustified(16, QLatin1Char('0'))));
}
}
if (tex_list.isEmpty())
textures_label->setText(QObject::tr("Textures: (none)"));
else
textures_label->setText(QObject::tr("Textures:\n%1").arg(tex_list.join(QStringLiteral("\n"))));
}
else
{
textures_label->setText(QObject::tr("Textures: (none)"));
}
};
connect(enable_check, &QCheckBox::toggled, dlg, [&hunter, update_ui](bool checked) {
hunter.SetElementMode(checked);
update_ui();
});
connect(prev_button, &QPushButton::clicked, dlg, [&hunter, update_ui]() {
hunter.PrevElement();
update_ui();
});
connect(next_button, &QPushButton::clicked, dlg, [&hunter, update_ui]() {
hunter.NextElement();
update_ui();
});
connect(mark_start_button, &QPushButton::clicked, dlg, [&hunter, update_ui]() {
hunter.MarkRangeStart();
update_ui();
});
connect(mark_end_button, &QPushButton::clicked, dlg, [&hunter, update_ui]() {
hunter.MarkRangeEnd();
update_ui();
});
connect(clear_range_button, &QPushButton::clicked, dlg, [&hunter, update_ui]() {
hunter.ClearRange();
update_ui();
});
connect(save_button, &QPushButton::clicked, dlg, [this, &hunter, dlg]() {
if (!hunter.IsElementMode())
{
QMessageBox::warning(
dlg, tr("Draw Call Filter"),
tr("Enable Draw Call Filter first, then choose a draw call or range."));
return;
}
int range_start = hunter.GetElementStart();
int range_end = hunter.GetElementEnd();
if (range_start < 0 || range_end < 0)
{
const int elem = hunter.GetSelectedElement();
if (elem < 0)
{
QMessageBox::warning(dlg, tr("Draw Call Filter"),
tr("No draw call is selected yet."));
return;
}
range_start = elem;
range_end = elem;
}
SetSelectedDrawCallRange(range_start, range_end, hunter.GetElementTotal());
QMessageBox::information(
dlg, tr("Draw Calls Saved"),
tr("Saved draw call range %1 - %2 to the current shader.\n"
"Press 'Save Shader' in Shader Hunter to write it to the config.")
.arg(range_start + 1)
.arg(range_end + 1));
});
auto* timer = new QTimer(dlg);
timer->setInterval(100);
connect(timer, &QTimer::timeout, dlg, update_ui);
timer->start();
auto* layout = new QVBoxLayout;
layout->addWidget(info_label);
layout->addWidget(enable_check);
layout->addWidget(draw_call_label);
layout->addLayout(nav_layout);
layout->addWidget(range_label);
layout->addLayout(range_buttons_layout);
layout->addWidget(textures_label);
layout->addWidget(buttons);
dlg->setLayout(layout);
update_ui();
dlg->show();
}
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<uint>(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<std::unordered_map<u64, std::string>>();
connect(dlg, &QObject::destroyed, this, []() {
ShaderHunter::GetInstance().SetTextureToolActive(false);
});
const auto update_selected_label = [this, tree]() {
std::vector<u64> 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<uint64_t>(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<u64, std::string> 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<u64> 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<qulonglong>(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<qulonglong>(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<uint>(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<uint>(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<uint>(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<u64> 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<uint64_t>(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<uint64_t>& 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<qulonglong>(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"))));
}
void ShaderHunterWidget::SetSelectedDrawCallRange(int start, int end, int total)
{
m_saved_element_start = start;
m_saved_element_end = end;
m_saved_element_total = total;
if (start < 0 || end < 0)
{
m_selected_draw_call_label->setVisible(false);
return;
}
m_selected_draw_call_label->setVisible(true);
if (total > 0)
{
m_selected_draw_call_label->setText(
tr("Selected Draw Call Range:\n%1 - %2 @%3")
.arg(start + 1)
.arg(end + 1)
.arg(total));
}
else
{
m_selected_draw_call_label->setText(
tr("Selected Draw Call Range:\n%1 - %2").arg(start + 1).arg(end + 1));
}
}