mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-18 12:01:59 +02:00
861 lines
33 KiB
C++
861 lines
33 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 <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);
|
|
|
|
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<int>(ShaderHunter::MatchMode::ShaderFamily));
|
|
m_match_mode_combo->addItem(tr("Exact Hash"),
|
|
static_cast<int>(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<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("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));
|
|
m_handling_combo->addItem(tr("Passthrough"),
|
|
static_cast<int>(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<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_match_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
|
[this](int) {
|
|
m_saved_texture_filters.clear();
|
|
SetSelectedTextureHashes({});
|
|
auto& hunter = ShaderHunter::GetInstance();
|
|
hunter.SetHuntingMatchMode(static_cast<ShaderHunter::MatchMode>(
|
|
m_match_mode_combo->currentData().toInt()));
|
|
UpdateDisplay();
|
|
});
|
|
connect(m_type_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
|
m_saved_texture_filters.clear();
|
|
SetSelectedTextureHashes({});
|
|
ShaderHunter::GetInstance().SetActiveType(static_cast<ShaderHunter::ShaderType>(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<int>(&QComboBox::currentIndexChanged), this,
|
|
[this](int) { SetSelectedTextureHashes(m_saved_texture_filters); });
|
|
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);
|
|
const auto match_mode = ShaderHunter::GetInstance().GetHuntingMatchMode();
|
|
const int match_mode_idx = m_match_mode_combo->findData(static_cast<int>(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<uint>(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<qulonglong>(*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<ShaderHunter::MatchMode>(
|
|
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<ShaderHunter::HandlingType>(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<float>(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<uint>(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<qulonglong>(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<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::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"))));
|
|
}
|