mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-18 19:01:37 +02:00
768 lines
29 KiB
C++
768 lines
29 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "DolphinQt/Debugger/CullingCodeFinderWidget.h"
|
|
|
|
#include <algorithm>
|
|
#include <QCheckBox>
|
|
#include <QClipboard>
|
|
#include <QCloseEvent>
|
|
#include <QDesktopServices>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QGroupBox>
|
|
#include <QGuiApplication>
|
|
#include <QHeaderView>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QProgressBar>
|
|
#include <QPushButton>
|
|
#include <QSignalBlocker>
|
|
#include <QSpinBox>
|
|
#include <QTableWidget>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <filesystem>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/IniFile.h"
|
|
#include "Core/ActionReplay.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/PowerPC/PPCSymbolDB.h"
|
|
#include "Core/System.h"
|
|
#include "DolphinQt/Host.h"
|
|
#include "VideoCommon/CullingCodeFinder.h"
|
|
|
|
namespace
|
|
{
|
|
u32 ParseOptionalHexField(const QString& text, bool* ok)
|
|
{
|
|
QString parsed_text = text.trimmed();
|
|
if (parsed_text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
|
parsed_text.remove(0, 2);
|
|
|
|
*ok = parsed_text.isEmpty();
|
|
return parsed_text.isEmpty() ? 0u : parsed_text.toUInt(ok, 16);
|
|
}
|
|
|
|
QString PathLeaf(QString path)
|
|
{
|
|
return QFileInfo(path).fileName();
|
|
}
|
|
|
|
QString PathLeaf(const std::string& path)
|
|
{
|
|
return QFileInfo(QString::fromStdString(path)).fileName();
|
|
}
|
|
} // namespace
|
|
|
|
CullingCodeFinderWidget::CullingCodeFinderWidget(QWidget* parent) : QDialog(parent)
|
|
{
|
|
setWindowTitle(tr("Culling Code Finder"));
|
|
setMinimumWidth(840);
|
|
|
|
CreateWidgets();
|
|
ConnectSignals();
|
|
RefreshStaticInfo();
|
|
UpdateButtonStates();
|
|
|
|
m_update_timer = new QTimer(this);
|
|
connect(m_update_timer, &QTimer::timeout, this, &CullingCodeFinderWidget::UpdateDisplay);
|
|
m_update_timer->start(16);
|
|
}
|
|
|
|
void CullingCodeFinderWidget::closeEvent(QCloseEvent* event)
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning ||
|
|
finder.GetState() == CullingCodeFinder::ScanState::Paused)
|
|
{
|
|
finder.StopScan();
|
|
finder.AdvanceScan(Core::System::GetInstance());
|
|
}
|
|
|
|
m_update_timer->stop();
|
|
event->accept();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::CreateWidgets()
|
|
{
|
|
auto* main_layout = new QVBoxLayout(this);
|
|
|
|
auto* stats_group = new QGroupBox(tr("Live Statistics"));
|
|
auto* stats_layout = new QHBoxLayout;
|
|
stats_group->setLayout(stats_layout);
|
|
|
|
stats_layout->addWidget(new QLabel(tr("Draw Calls:")));
|
|
m_live_draw_calls = new QLabel(QStringLiteral("0"));
|
|
stats_layout->addWidget(m_live_draw_calls);
|
|
stats_layout->addWidget(new QLabel(tr("Triangles:")));
|
|
m_live_triangles = new QLabel(QStringLiteral("0"));
|
|
stats_layout->addWidget(m_live_triangles);
|
|
stats_layout->addWidget(new QLabel(tr("Prims:")));
|
|
m_live_prims = new QLabel(QStringLiteral("0"));
|
|
stats_layout->addWidget(m_live_prims);
|
|
stats_layout->addWidget(new QLabel(tr("Objects:")));
|
|
m_live_objects = new QLabel(QStringLiteral("0"));
|
|
stats_layout->addWidget(m_live_objects);
|
|
stats_layout->addStretch();
|
|
main_layout->addWidget(stats_group);
|
|
|
|
auto* baseline_group = new QGroupBox(tr("Baseline"));
|
|
auto* baseline_layout = new QHBoxLayout;
|
|
baseline_group->setLayout(baseline_layout);
|
|
m_baseline_label = new QLabel(tr("Not set"));
|
|
baseline_layout->addWidget(m_baseline_label);
|
|
baseline_layout->addStretch();
|
|
m_set_baseline_btn = new QPushButton(tr("Set Baseline"));
|
|
baseline_layout->addWidget(m_set_baseline_btn);
|
|
main_layout->addWidget(baseline_group);
|
|
|
|
auto* prerequisite_group = new QGroupBox(tr("Prerequisites"));
|
|
auto* prerequisite_layout = new QVBoxLayout;
|
|
prerequisite_group->setLayout(prerequisite_layout);
|
|
|
|
auto* map_layout = new QHBoxLayout;
|
|
map_layout->addWidget(new QLabel(tr("Map File:")));
|
|
m_map_path_label = new QLabel(tr("Not found"));
|
|
m_map_path_label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
map_layout->addWidget(m_map_path_label, 1);
|
|
m_generate_map_btn = new QPushButton(tr("Generate .map"));
|
|
map_layout->addWidget(m_generate_map_btn);
|
|
prerequisite_layout->addLayout(map_layout);
|
|
|
|
auto* output_layout = new QHBoxLayout;
|
|
output_layout->addWidget(new QLabel(tr("Output Folder:")));
|
|
m_output_folder_edit = new QLineEdit;
|
|
output_layout->addWidget(m_output_folder_edit, 1);
|
|
m_browse_output_btn = new QPushButton(tr("Browse"));
|
|
m_open_output_btn = new QPushButton(tr("Open"));
|
|
output_layout->addWidget(m_browse_output_btn);
|
|
output_layout->addWidget(m_open_output_btn);
|
|
prerequisite_layout->addLayout(output_layout);
|
|
main_layout->addWidget(prerequisite_group);
|
|
|
|
auto* config_group = new QGroupBox(tr("Scan Configuration"));
|
|
auto* config_layout = new QVBoxLayout;
|
|
config_group->setLayout(config_layout);
|
|
|
|
auto* top_row = new QHBoxLayout;
|
|
top_row->addWidget(new QLabel(tr("Savestate Slot:")));
|
|
m_savestate_slot_spin = new QSpinBox;
|
|
m_savestate_slot_spin->setRange(1, 10);
|
|
m_savestate_slot_spin->setValue(1);
|
|
top_row->addWidget(m_savestate_slot_spin);
|
|
top_row->addWidget(new QLabel(tr("Settle Frames:")));
|
|
m_settle_frames_spin = new QSpinBox;
|
|
m_settle_frames_spin->setRange(1, 60);
|
|
m_settle_frames_spin->setValue(5);
|
|
top_row->addWidget(m_settle_frames_spin);
|
|
top_row->addWidget(new QLabel(tr("Candidate Limit:")));
|
|
m_candidate_limit_spin = new QSpinBox;
|
|
m_candidate_limit_spin->setRange(0, 1000000);
|
|
m_candidate_limit_spin->setValue(0);
|
|
m_candidate_limit_spin->setSpecialValueText(tr("Unlimited"));
|
|
top_row->addWidget(m_candidate_limit_spin);
|
|
top_row->addStretch();
|
|
config_layout->addLayout(top_row);
|
|
|
|
m_settle_frames_help = new QLabel(
|
|
tr("Settle Frames are emulated frames after each savestate reload. Turbo only reduces wall-"
|
|
"clock scan time."));
|
|
m_settle_frames_help->setWordWrap(true);
|
|
config_layout->addWidget(m_settle_frames_help);
|
|
|
|
auto* threshold_row = new QHBoxLayout;
|
|
threshold_row->addWidget(new QLabel(tr("Min Draw Calls +:")));
|
|
m_min_draw_call_spin = new QSpinBox;
|
|
m_min_draw_call_spin->setRange(1, 1000000);
|
|
m_min_draw_call_spin->setValue(1);
|
|
m_min_draw_call_spin->setToolTip(
|
|
tr("Minimum increase in draw calls versus the baseline required to keep a result."));
|
|
threshold_row->addWidget(m_min_draw_call_spin);
|
|
threshold_row->addWidget(new QLabel(tr("Min Triangles +:")));
|
|
m_min_triangle_spin = new QSpinBox;
|
|
m_min_triangle_spin->setRange(0, 100000000);
|
|
m_min_triangle_spin->setValue(0);
|
|
m_min_triangle_spin->setToolTip(
|
|
tr("Minimum increase in drawn triangles versus the baseline required to keep a result."));
|
|
threshold_row->addWidget(m_min_triangle_spin);
|
|
threshold_row->addWidget(new QLabel(tr("Min Objects +:")));
|
|
m_min_object_spin = new QSpinBox;
|
|
m_min_object_spin->setRange(0, 1000000);
|
|
m_min_object_spin->setValue(0);
|
|
m_min_object_spin->setToolTip(
|
|
tr("Minimum increase in drawn objects versus the baseline required to keep a result."));
|
|
threshold_row->addWidget(m_min_object_spin);
|
|
threshold_row->addStretch();
|
|
config_layout->addLayout(threshold_row);
|
|
|
|
auto* filter_row = new QHBoxLayout;
|
|
filter_row->addWidget(new QLabel(tr("Start Address:")));
|
|
m_start_address_edit = new QLineEdit;
|
|
m_start_address_edit->setPlaceholderText(QStringLiteral("0 or 80052FE8"));
|
|
filter_row->addWidget(m_start_address_edit);
|
|
filter_row->addWidget(new QLabel(tr("End Address:")));
|
|
m_end_address_edit = new QLineEdit;
|
|
m_end_address_edit->setPlaceholderText(QStringLiteral("0 or 80053000"));
|
|
filter_row->addWidget(m_end_address_edit);
|
|
filter_row->addWidget(new QLabel(tr("Symbol Filter:")));
|
|
m_symbol_filter_edit = new QLineEdit;
|
|
filter_row->addWidget(m_symbol_filter_edit, 1);
|
|
config_layout->addLayout(filter_row);
|
|
|
|
auto* runtime_row = new QHBoxLayout;
|
|
m_turbo_during_scan_check = new QCheckBox(tr("Turbo During Scan"));
|
|
m_turbo_during_scan_check->setChecked(true);
|
|
runtime_row->addWidget(m_turbo_during_scan_check);
|
|
m_mute_audio_during_scan_check = new QCheckBox(tr("Mute Audio During Scan"));
|
|
m_mute_audio_during_scan_check->setChecked(true);
|
|
runtime_row->addWidget(m_mute_audio_during_scan_check);
|
|
m_auto_capture_reference_state_check = new QCheckBox(tr("Auto-Capture Reference State"));
|
|
m_auto_capture_reference_state_check->setChecked(true);
|
|
runtime_row->addWidget(m_auto_capture_reference_state_check);
|
|
runtime_row->addStretch();
|
|
config_layout->addLayout(runtime_row);
|
|
|
|
auto* template_row = new QHBoxLayout;
|
|
constexpr std::array<const char*, 4> labels = {"38600001 + BLR", "38600000 + BLR",
|
|
"60000001 + BLR", "60000000 + BLR"};
|
|
for (size_t i = 0; i < labels.size(); ++i)
|
|
{
|
|
m_template_checks[i] = new QCheckBox(QString::fromLatin1(labels[i]));
|
|
m_template_checks[i]->setChecked(true);
|
|
template_row->addWidget(m_template_checks[i]);
|
|
}
|
|
template_row->addStretch();
|
|
config_layout->addLayout(template_row);
|
|
main_layout->addWidget(config_group);
|
|
|
|
auto* controls_layout = new QHBoxLayout;
|
|
m_start_btn = new QPushButton(tr("Start Scan"));
|
|
m_restore_btn = new QPushButton(tr("Restore Session"));
|
|
m_pause_btn = new QPushButton(tr("Pause"));
|
|
m_stop_btn = new QPushButton(tr("Stop"));
|
|
controls_layout->addWidget(m_start_btn);
|
|
controls_layout->addWidget(m_restore_btn);
|
|
controls_layout->addWidget(m_pause_btn);
|
|
controls_layout->addWidget(m_stop_btn);
|
|
controls_layout->addStretch();
|
|
main_layout->addLayout(controls_layout);
|
|
|
|
m_progress_bar = new QProgressBar;
|
|
m_progress_bar->setRange(0, 100);
|
|
m_progress_bar->setValue(0);
|
|
main_layout->addWidget(m_progress_bar);
|
|
|
|
m_status_label = new QLabel(tr("Idle"));
|
|
main_layout->addWidget(m_status_label);
|
|
|
|
auto* results_group = new QGroupBox(tr("Results"));
|
|
auto* results_layout = new QVBoxLayout;
|
|
results_group->setLayout(results_layout);
|
|
|
|
m_results_table = new QTableWidget(0, 8);
|
|
m_results_table->setHorizontalHeaderLabels(
|
|
{tr("Symbol"), tr("Address"), tr("Template"), tr("DC Delta"), tr("Tri Delta"),
|
|
tr("Obj Delta"), tr("Timeout"), tr("Screenshot")});
|
|
m_results_table->setSelectionBehavior(QTableWidget::SelectRows);
|
|
m_results_table->setSelectionMode(QTableWidget::ExtendedSelection);
|
|
m_results_table->setEditTriggers(QTableWidget::NoEditTriggers);
|
|
m_results_table->horizontalHeader()->setStretchLastSection(true);
|
|
m_results_table->verticalHeader()->setVisible(false);
|
|
results_layout->addWidget(m_results_table);
|
|
|
|
m_show_timeout_results_check = new QCheckBox(tr("Show Timeout Results"));
|
|
m_show_timeout_results_check->setChecked(false);
|
|
results_layout->addWidget(m_show_timeout_results_check);
|
|
|
|
auto* result_buttons = new QHBoxLayout;
|
|
m_generate_ar_btn = new QPushButton(tr("Generate AR Code"));
|
|
m_reset_results_btn = new QPushButton(tr("Reset Results"));
|
|
result_buttons->addWidget(m_generate_ar_btn);
|
|
result_buttons->addWidget(m_reset_results_btn);
|
|
result_buttons->addStretch();
|
|
results_layout->addLayout(result_buttons);
|
|
main_layout->addWidget(results_group);
|
|
}
|
|
|
|
void CullingCodeFinderWidget::ConnectSignals()
|
|
{
|
|
connect(m_set_baseline_btn, &QPushButton::clicked, this,
|
|
&CullingCodeFinderWidget::OnSetBaseline);
|
|
connect(m_generate_map_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnGenerateMap);
|
|
connect(m_start_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStartScan);
|
|
connect(m_restore_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnRestoreSession);
|
|
connect(m_pause_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnPauseScan);
|
|
connect(m_stop_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStopScan);
|
|
connect(m_generate_ar_btn, &QPushButton::clicked, this,
|
|
&CullingCodeFinderWidget::OnGenerateARCode);
|
|
connect(m_reset_results_btn, &QPushButton::clicked, this,
|
|
&CullingCodeFinderWidget::OnResetResults);
|
|
connect(m_show_timeout_results_check, &QCheckBox::toggled, this, [this] {
|
|
UpdateResultsTable();
|
|
UpdateButtonStates();
|
|
});
|
|
connect(m_browse_output_btn, &QPushButton::clicked, this,
|
|
&CullingCodeFinderWidget::OnBrowseOutputFolder);
|
|
connect(m_open_output_btn, &QPushButton::clicked, this,
|
|
&CullingCodeFinderWidget::OnOpenOutputFolder);
|
|
}
|
|
|
|
void CullingCodeFinderWidget::RefreshStaticInfo()
|
|
{
|
|
const std::string game_id = SConfig::GetInstance().GetGameID();
|
|
std::string map_path;
|
|
PPCSymbolDB::FindMapFile(&map_path, nullptr);
|
|
if (map_path.empty())
|
|
{
|
|
const std::string finder_map_path = CullingCodeFinder::GetInstance().GetMapPath();
|
|
const std::string expected_suffix = game_id + ".map";
|
|
if (!finder_map_path.empty() && !game_id.empty() && finder_map_path.ends_with(expected_suffix))
|
|
map_path = finder_map_path;
|
|
}
|
|
|
|
m_map_path_label->setText(map_path.empty() ? tr("Not found") : QString::fromStdString(map_path));
|
|
|
|
if (m_output_folder_edit->text().isEmpty() && !game_id.empty())
|
|
{
|
|
m_output_folder_edit->setText(
|
|
QString::fromStdString(CullingCodeFinder::GetDefaultOutputFolder(game_id)));
|
|
}
|
|
}
|
|
|
|
void CullingCodeFinderWidget::UpdateDisplay()
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
const auto stats = finder.GetLatestStats();
|
|
m_live_draw_calls->setText(QString::number(stats.draw_calls));
|
|
m_live_triangles->setText(QString::number(stats.triangles_drawn));
|
|
m_live_prims->setText(QString::number(stats.prims));
|
|
m_live_objects->setText(QString::number(stats.drawn_objects));
|
|
|
|
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
|
|
finder.AdvanceScan(Core::System::GetInstance());
|
|
|
|
const auto baseline = finder.GetBaseline();
|
|
m_baseline_label->setText(finder.HasBaseline() ?
|
|
tr("DC: %1 | Tri: %2 | Prims: %3 | Obj: %4")
|
|
.arg(baseline.draw_calls)
|
|
.arg(baseline.triangles_drawn)
|
|
.arg(baseline.prims)
|
|
.arg(baseline.drawn_objects) :
|
|
tr("Not set"));
|
|
|
|
m_progress_bar->setValue(static_cast<int>(finder.GetProgress() * 100.0f));
|
|
m_status_label->setText(QString::fromStdString(finder.GetStatusText()));
|
|
|
|
const int result_count = finder.GetResultCount();
|
|
if (result_count != m_last_result_count)
|
|
{
|
|
UpdateResultsTable();
|
|
m_last_result_count = result_count;
|
|
}
|
|
|
|
RefreshStaticInfo();
|
|
UpdateButtonStates();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnSetBaseline()
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
finder.SetBaseline();
|
|
UpdateDisplay();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnStartScan()
|
|
{
|
|
CullingCodeFinder::ScanConfig config;
|
|
bool start_address_ok = false;
|
|
bool end_address_ok = false;
|
|
const u32 parsed_start_address =
|
|
ParseOptionalHexField(m_start_address_edit->text(), &start_address_ok);
|
|
const u32 parsed_end_address = ParseOptionalHexField(m_end_address_edit->text(), &end_address_ok);
|
|
if (!start_address_ok)
|
|
{
|
|
QMessageBox::warning(this, tr("Invalid Start Address"),
|
|
tr("Enter the start address as hexadecimal, for example 80052FE8."));
|
|
return;
|
|
}
|
|
if (!end_address_ok)
|
|
{
|
|
QMessageBox::warning(this, tr("Invalid End Address"),
|
|
tr("Enter the end address as hexadecimal, for example 80053000."));
|
|
return;
|
|
}
|
|
if (parsed_start_address != 0 && parsed_end_address != 0 &&
|
|
parsed_start_address > parsed_end_address)
|
|
{
|
|
QMessageBox::warning(this, tr("Invalid Address Range"),
|
|
tr("The start address must be lower than or equal to the end address."));
|
|
return;
|
|
}
|
|
|
|
config.savestate_slot = m_savestate_slot_spin->value();
|
|
config.settle_frames = m_settle_frames_spin->value();
|
|
config.min_draw_call_increase = m_min_draw_call_spin->value();
|
|
config.min_triangle_increase = m_min_triangle_spin->value();
|
|
config.min_object_increase = m_min_object_spin->value();
|
|
config.candidate_limit = m_candidate_limit_spin->value();
|
|
config.start_address = parsed_start_address;
|
|
config.end_address = parsed_end_address;
|
|
config.turbo_during_scan = m_turbo_during_scan_check->isChecked();
|
|
config.mute_audio_during_scan = m_mute_audio_during_scan_check->isChecked();
|
|
config.auto_capture_reference_state = m_auto_capture_reference_state_check->isChecked();
|
|
config.symbol_filter = m_symbol_filter_edit->text().trimmed().toStdString();
|
|
config.output_folder = m_output_folder_edit->text().trimmed().toStdString();
|
|
|
|
for (size_t i = 0; i < m_template_checks.size(); ++i)
|
|
config.enabled_templates[i] = m_template_checks[i]->isChecked();
|
|
|
|
std::string error;
|
|
if (!CullingCodeFinder::GetInstance().StartScan(Core::System::GetInstance(), std::move(config),
|
|
&error))
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Start Scan"), QString::fromStdString(error));
|
|
return;
|
|
}
|
|
|
|
m_last_result_count = -1;
|
|
UpdateDisplay();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnRestoreSession()
|
|
{
|
|
const QString output_folder = m_output_folder_edit->text().trimmed();
|
|
if (output_folder.isEmpty())
|
|
{
|
|
QMessageBox::warning(this, tr("No Output Folder"),
|
|
tr("Select the scan output folder that contains manifest.ini first."));
|
|
return;
|
|
}
|
|
|
|
std::string error;
|
|
std::string restored_message;
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
if (!finder.RestoreSession(Core::System::GetInstance(), output_folder.toStdString(), &error,
|
|
&restored_message))
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Restore Session"), QString::fromStdString(error));
|
|
return;
|
|
}
|
|
|
|
ApplyConfigToWidgets();
|
|
m_last_result_count = -1;
|
|
finder.AdvanceScan(Core::System::GetInstance());
|
|
UpdateDisplay();
|
|
|
|
QMessageBox::information(this, tr("Session Restored"),
|
|
QString::fromStdString(restored_message));
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnGenerateMap()
|
|
{
|
|
std::string error;
|
|
if (!CullingCodeFinder::GetInstance().GenerateSymbolMap(Core::System::GetInstance(), &error))
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Generate Map"), QString::fromStdString(error));
|
|
return;
|
|
}
|
|
|
|
emit Host::GetInstance()->PPCSymbolsChanged();
|
|
RefreshStaticInfo();
|
|
QMessageBox::information(this, tr("Map Generated"),
|
|
tr("Rebuilt and saved the writable symbol map for the running game."));
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnPauseScan()
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
|
|
{
|
|
finder.PauseScan();
|
|
finder.AdvanceScan(Core::System::GetInstance());
|
|
}
|
|
else if (finder.GetState() == CullingCodeFinder::ScanState::Paused)
|
|
{
|
|
finder.ResumeScan();
|
|
}
|
|
|
|
UpdateDisplay();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnStopScan()
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
finder.StopScan();
|
|
finder.AdvanceScan(Core::System::GetInstance());
|
|
UpdateDisplay();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnGenerateARCode()
|
|
{
|
|
const std::string game_id = SConfig::GetInstance().GetGameID();
|
|
if (game_id.empty())
|
|
{
|
|
QMessageBox::warning(this, tr("No Game Running"),
|
|
tr("Boot a game before generating Action Replay codes."));
|
|
return;
|
|
}
|
|
|
|
const QModelIndexList selected_rows = m_results_table->selectionModel()->selectedRows();
|
|
if (selected_rows.empty())
|
|
{
|
|
QMessageBox::information(this, tr("No Results Selected"),
|
|
tr("Select one or more results first."));
|
|
return;
|
|
}
|
|
|
|
const auto results = CullingCodeFinder::GetInstance().GetResults();
|
|
std::vector<int> row_indices;
|
|
row_indices.reserve(selected_rows.size());
|
|
for (const QModelIndex& index : selected_rows)
|
|
row_indices.push_back(index.row());
|
|
std::ranges::sort(row_indices);
|
|
|
|
std::vector<ActionReplay::ARCode> generated_codes;
|
|
generated_codes.reserve(row_indices.size());
|
|
QString clipboard_text;
|
|
|
|
for (int row : row_indices)
|
|
{
|
|
if (row < 0 || row >= static_cast<int>(m_visible_result_indices.size()))
|
|
continue;
|
|
|
|
const int result_index = m_visible_result_indices[static_cast<size_t>(row)];
|
|
if (result_index < 0 || result_index >= static_cast<int>(results.size()))
|
|
continue;
|
|
|
|
const auto& result = results[static_cast<size_t>(result_index)];
|
|
const auto lines = CullingCodeFinder::BuildARCodeLines(result.address, result.template_kind);
|
|
|
|
ActionReplay::ARCode code;
|
|
code.name =
|
|
fmt::format("VR Culling - {} [{:08X}] {}", result.symbol_name, result.address,
|
|
CullingCodeFinder::GetPatchTemplateName(result.template_kind));
|
|
code.enabled = true;
|
|
code.default_enabled = false;
|
|
code.user_defined = true;
|
|
|
|
for (const std::string& line : lines)
|
|
{
|
|
const auto parsed = ActionReplay::DeserializeLine(line);
|
|
if (const auto* entry = std::get_if<ActionReplay::AREntry>(&parsed))
|
|
code.ops.push_back(*entry);
|
|
}
|
|
|
|
if (code.ops.size() != lines.size())
|
|
continue;
|
|
|
|
generated_codes.push_back(std::move(code));
|
|
|
|
if (!clipboard_text.isEmpty())
|
|
clipboard_text.append(QStringLiteral("\n\n"));
|
|
clipboard_text.append(QString::fromStdString(result.ar_code_text));
|
|
}
|
|
|
|
if (generated_codes.empty())
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Generate Codes"),
|
|
tr("The selected results did not produce valid AR code lines."));
|
|
return;
|
|
}
|
|
|
|
Common::IniFile game_ini_local;
|
|
const Common::IniFile game_ini_default =
|
|
SConfig::LoadDefaultGameIni(game_id, SConfig::GetInstance().GetRevision());
|
|
const std::string ini_path =
|
|
std::string(File::GetUserPath(D_GAMESETTINGS_IDX)).append(game_id).append(".ini");
|
|
game_ini_local.Load(ini_path);
|
|
|
|
std::vector<ActionReplay::ARCode> codes = ActionReplay::LoadCodes(game_ini_default, game_ini_local);
|
|
|
|
for (ActionReplay::ARCode& code : generated_codes)
|
|
{
|
|
std::erase_if(codes, [&](const ActionReplay::ARCode& existing_code) {
|
|
return existing_code.user_defined && existing_code.name == code.name;
|
|
});
|
|
codes.push_back(std::move(code));
|
|
}
|
|
|
|
ActionReplay::SaveCodes(&game_ini_local, codes);
|
|
if (!game_ini_local.Save(ini_path))
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Save Codes"),
|
|
tr("Failed to save generated codes to the local game INI."));
|
|
return;
|
|
}
|
|
|
|
ActionReplay::ApplyCodes(codes, game_id, SConfig::GetInstance().GetRevision());
|
|
QGuiApplication::clipboard()->setText(clipboard_text);
|
|
|
|
QMessageBox::information(this, tr("AR Codes Generated"),
|
|
tr("Saved %1 code(s) and copied the generated text to the clipboard.")
|
|
.arg(generated_codes.size()));
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnBrowseOutputFolder()
|
|
{
|
|
const QString current_path = m_output_folder_edit->text().trimmed();
|
|
const QString selected_path =
|
|
QFileDialog::getExistingDirectory(this, tr("Select Output Folder"), current_path);
|
|
if (!selected_path.isEmpty())
|
|
m_output_folder_edit->setText(selected_path);
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnOpenOutputFolder()
|
|
{
|
|
const QString output_path = m_output_folder_edit->text().trimmed();
|
|
if (output_path.isEmpty())
|
|
return;
|
|
|
|
File::CreateFullPath(output_path.toStdString() + DIR_SEP);
|
|
QDesktopServices::openUrl(QUrl::fromLocalFile(output_path));
|
|
}
|
|
|
|
void CullingCodeFinderWidget::OnResetResults()
|
|
{
|
|
auto& finder = CullingCodeFinder::GetInstance();
|
|
std::string error;
|
|
finder.ClearResults(&error);
|
|
if (!error.empty())
|
|
{
|
|
QMessageBox::warning(this, tr("Unable to Reset Results"), QString::fromStdString(error));
|
|
return;
|
|
}
|
|
|
|
m_last_result_count = -1;
|
|
UpdateDisplay();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::UpdateResultsTable()
|
|
{
|
|
const auto results = CullingCodeFinder::GetInstance().GetResults();
|
|
const bool show_timeouts =
|
|
m_show_timeout_results_check && m_show_timeout_results_check->isChecked();
|
|
|
|
m_visible_result_indices.clear();
|
|
m_visible_result_indices.reserve(results.size());
|
|
for (int i = 0; i < static_cast<int>(results.size()); ++i)
|
|
{
|
|
if (!show_timeouts && results[static_cast<size_t>(i)].caused_timeout)
|
|
continue;
|
|
m_visible_result_indices.push_back(i);
|
|
}
|
|
|
|
QSignalBlocker blocker(m_results_table);
|
|
m_results_table->clearSelection();
|
|
m_results_table->setRowCount(static_cast<int>(m_visible_result_indices.size()));
|
|
|
|
for (int row = 0; row < static_cast<int>(m_visible_result_indices.size()); ++row)
|
|
{
|
|
const auto& result = results[static_cast<size_t>(m_visible_result_indices[row])];
|
|
auto* symbol_item = new QTableWidgetItem(QString::fromStdString(result.symbol_name));
|
|
auto* address_item = new QTableWidgetItem(QString::asprintf("%08X", result.address));
|
|
auto* template_item = new QTableWidgetItem(
|
|
QString::fromStdString(CullingCodeFinder::GetPatchTemplateName(result.template_kind)));
|
|
auto* draw_call_item = new QTableWidgetItem(QString::number(result.draw_call_delta));
|
|
auto* triangle_item = new QTableWidgetItem(QString::number(result.triangle_delta));
|
|
auto* object_item = new QTableWidgetItem(QString::number(result.object_delta));
|
|
auto* timeout_item = new QTableWidgetItem(result.caused_timeout ? tr("Yes") : tr("No"));
|
|
auto* screenshot_item = new QTableWidgetItem(PathLeaf(result.screenshot_path));
|
|
|
|
address_item->setTextAlignment(Qt::AlignCenter);
|
|
draw_call_item->setTextAlignment(Qt::AlignCenter);
|
|
triangle_item->setTextAlignment(Qt::AlignCenter);
|
|
object_item->setTextAlignment(Qt::AlignCenter);
|
|
timeout_item->setTextAlignment(Qt::AlignCenter);
|
|
|
|
m_results_table->setItem(row, 0, symbol_item);
|
|
m_results_table->setItem(row, 1, address_item);
|
|
m_results_table->setItem(row, 2, template_item);
|
|
m_results_table->setItem(row, 3, draw_call_item);
|
|
m_results_table->setItem(row, 4, triangle_item);
|
|
m_results_table->setItem(row, 5, object_item);
|
|
m_results_table->setItem(row, 6, timeout_item);
|
|
m_results_table->setItem(row, 7, screenshot_item);
|
|
}
|
|
|
|
m_results_table->resizeColumnsToContents();
|
|
}
|
|
|
|
void CullingCodeFinderWidget::UpdateButtonStates()
|
|
{
|
|
const auto state = CullingCodeFinder::GetInstance().GetState();
|
|
const bool actively_scanning = state == CullingCodeFinder::ScanState::Scanning;
|
|
const bool paused = state == CullingCodeFinder::ScanState::Paused;
|
|
const bool busy = actively_scanning || paused;
|
|
|
|
m_set_baseline_btn->setEnabled(!busy);
|
|
m_generate_map_btn->setEnabled(!busy);
|
|
m_savestate_slot_spin->setEnabled(!busy);
|
|
m_settle_frames_spin->setEnabled(!busy);
|
|
m_min_draw_call_spin->setEnabled(!busy);
|
|
m_min_triangle_spin->setEnabled(!busy);
|
|
m_min_object_spin->setEnabled(!busy);
|
|
m_candidate_limit_spin->setEnabled(!busy);
|
|
m_start_address_edit->setEnabled(!busy);
|
|
m_end_address_edit->setEnabled(!busy);
|
|
m_symbol_filter_edit->setEnabled(!busy);
|
|
m_turbo_during_scan_check->setEnabled(!busy);
|
|
m_mute_audio_during_scan_check->setEnabled(!busy);
|
|
m_auto_capture_reference_state_check->setEnabled(!busy);
|
|
m_output_folder_edit->setEnabled(!busy);
|
|
m_browse_output_btn->setEnabled(!busy);
|
|
for (QCheckBox* checkbox : m_template_checks)
|
|
{
|
|
if (checkbox)
|
|
checkbox->setEnabled(!busy);
|
|
}
|
|
|
|
m_start_btn->setEnabled(!busy);
|
|
m_restore_btn->setEnabled(!busy && !m_output_folder_edit->text().trimmed().isEmpty());
|
|
m_pause_btn->setEnabled(actively_scanning || paused);
|
|
m_pause_btn->setText(paused ? tr("Resume") : tr("Pause"));
|
|
m_stop_btn->setEnabled(busy);
|
|
m_show_timeout_results_check->setEnabled(true);
|
|
m_generate_ar_btn->setEnabled(m_results_table->rowCount() > 0);
|
|
m_reset_results_btn->setEnabled(!busy && !CullingCodeFinder::GetInstance().GetResults().empty());
|
|
m_open_output_btn->setEnabled(!m_output_folder_edit->text().trimmed().isEmpty());
|
|
}
|
|
|
|
void CullingCodeFinderWidget::ApplyConfigToWidgets()
|
|
{
|
|
const auto config = CullingCodeFinder::GetInstance().GetConfig();
|
|
|
|
m_savestate_slot_spin->setValue(config.savestate_slot);
|
|
m_settle_frames_spin->setValue(config.settle_frames);
|
|
m_min_draw_call_spin->setValue(config.min_draw_call_increase);
|
|
m_min_triangle_spin->setValue(config.min_triangle_increase);
|
|
m_min_object_spin->setValue(config.min_object_increase);
|
|
m_candidate_limit_spin->setValue(config.candidate_limit);
|
|
m_start_address_edit->setText(config.start_address == 0 ?
|
|
QString{} :
|
|
QString::asprintf("%08X", config.start_address));
|
|
m_end_address_edit->setText(config.end_address == 0 ?
|
|
QString{} :
|
|
QString::asprintf("%08X", config.end_address));
|
|
m_symbol_filter_edit->setText(QString::fromStdString(config.symbol_filter));
|
|
m_turbo_during_scan_check->setChecked(config.turbo_during_scan);
|
|
m_mute_audio_during_scan_check->setChecked(config.mute_audio_during_scan);
|
|
m_auto_capture_reference_state_check->setChecked(config.auto_capture_reference_state);
|
|
m_output_folder_edit->setText(QString::fromStdString(config.output_folder));
|
|
|
|
for (size_t i = 0; i < m_template_checks.size(); ++i)
|
|
{
|
|
if (m_template_checks[i])
|
|
m_template_checks[i]->setChecked(config.enabled_templates[i]);
|
|
}
|
|
}
|