mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-18 01:02:26 +02:00
1504 lines
50 KiB
C++
1504 lines
50 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "VideoCommon/CullingCodeFinder.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "AudioCommon/AudioCommon.h"
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/Config/Config.h"
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/IniFile.h"
|
|
#include "Common/Logging/Log.h"
|
|
#include "Common/StringUtil.h"
|
|
#include "Core/Config/MainSettings.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/HW/Memmap.h"
|
|
#include "Core/PowerPC/JitInterface.h"
|
|
#include "Core/PowerPC/MMU.h"
|
|
#include "Core/PowerPC/PPCAnalyst.h"
|
|
#include "Core/PowerPC/PPCSymbolDB.h"
|
|
#include "Core/PowerPC/SignatureDB/SignatureDB.h"
|
|
#include "Core/State.h"
|
|
#include "Core/System.h"
|
|
#include "VideoCommon/FrameDumper.h"
|
|
#include "VideoCommon/Statistics.h"
|
|
|
|
namespace
|
|
{
|
|
using PatchTemplateKind = CullingCodeFinder::PatchTemplateKind;
|
|
|
|
constexpr size_t TEMPLATE_COUNT = static_cast<size_t>(PatchTemplateKind::Count);
|
|
constexpr std::array<CullingCodeFinder::PatchTemplateKind, TEMPLATE_COUNT> TEMPLATE_ORDER = {
|
|
PatchTemplateKind::LiR3True, PatchTemplateKind::LiR3False, PatchTemplateKind::OriR0True,
|
|
PatchTemplateKind::OriR0False};
|
|
|
|
constexpr u32 BLR_INSTRUCTION = 0x4E800020u;
|
|
constexpr char MANIFEST_SECTION_SESSION[] = "Session";
|
|
constexpr char MANIFEST_SECTION_BASELINE[] = "Baseline";
|
|
constexpr char MANIFEST_PREFIX_CANDIDATE[] = "Candidate_";
|
|
constexpr char MANIFEST_PREFIX_RESULT[] = "Result_";
|
|
|
|
std::array<CullingCodeFinder::PatchTemplate, TEMPLATE_COUNT> GetPatchTemplates()
|
|
{
|
|
return {{
|
|
{PatchTemplateKind::LiR3True, "38600001 + BLR", 0x38600001u},
|
|
{PatchTemplateKind::LiR3False, "38600000 + BLR", 0x38600000u},
|
|
{PatchTemplateKind::OriR0True, "60000001 + BLR", 0x60000001u},
|
|
{PatchTemplateKind::OriR0False, "60000000 + BLR", 0x60000000u},
|
|
}};
|
|
}
|
|
|
|
const CullingCodeFinder::PatchTemplate& LookupTemplate(PatchTemplateKind kind)
|
|
{
|
|
static const auto templates = GetPatchTemplates();
|
|
return templates[static_cast<size_t>(kind)];
|
|
}
|
|
|
|
std::string MakeSectionName(std::string_view prefix, size_t index)
|
|
{
|
|
return fmt::format("{}{}", prefix, index);
|
|
}
|
|
|
|
std::string MakeTemplateMask(
|
|
const std::array<bool, static_cast<size_t>(PatchTemplateKind::Count)>& enabled_templates)
|
|
{
|
|
std::string mask;
|
|
mask.reserve(TEMPLATE_COUNT);
|
|
for (bool enabled : enabled_templates)
|
|
mask.push_back(enabled ? '1' : '0');
|
|
return mask;
|
|
}
|
|
|
|
std::array<bool, static_cast<size_t>(PatchTemplateKind::Count)> ParseTemplateMask(
|
|
std::string_view mask)
|
|
{
|
|
std::array<bool, static_cast<size_t>(PatchTemplateKind::Count)> enabled_templates = {true, true,
|
|
true,
|
|
true};
|
|
if (mask.size() != TEMPLATE_COUNT)
|
|
return enabled_templates;
|
|
|
|
for (size_t i = 0; i < TEMPLATE_COUNT; ++i)
|
|
enabled_templates[i] = (mask[i] != '0');
|
|
return enabled_templates;
|
|
}
|
|
|
|
std::string StateToString(CullingCodeFinder::ScanState state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case CullingCodeFinder::ScanState::Idle:
|
|
return "Idle";
|
|
case CullingCodeFinder::ScanState::Scanning:
|
|
return "Scanning";
|
|
case CullingCodeFinder::ScanState::Paused:
|
|
return "Paused";
|
|
case CullingCodeFinder::ScanState::Stopped:
|
|
return "Stopped";
|
|
case CullingCodeFinder::ScanState::Finished:
|
|
return "Finished";
|
|
case CullingCodeFinder::ScanState::Error:
|
|
return "Error";
|
|
}
|
|
|
|
return "Idle";
|
|
}
|
|
|
|
CullingCodeFinder::ScanState StringToState(std::string_view state)
|
|
{
|
|
if (state == "Scanning")
|
|
return CullingCodeFinder::ScanState::Scanning;
|
|
if (state == "Paused")
|
|
return CullingCodeFinder::ScanState::Paused;
|
|
if (state == "Stopped")
|
|
return CullingCodeFinder::ScanState::Stopped;
|
|
if (state == "Finished")
|
|
return CullingCodeFinder::ScanState::Finished;
|
|
if (state == "Error")
|
|
return CullingCodeFinder::ScanState::Error;
|
|
return CullingCodeFinder::ScanState::Idle;
|
|
}
|
|
|
|
std::string ScreenshotBaseName(const CullingCodeFinder::Candidate& candidate,
|
|
PatchTemplateKind template_kind)
|
|
{
|
|
std::string template_name = CullingCodeFinder::GetPatchTemplateName(template_kind);
|
|
Common::ToLower(&template_name);
|
|
template_name = ReplaceAll(template_name, " ", "_");
|
|
template_name = ReplaceAll(template_name, "+", "");
|
|
return fmt::format("{:08X}_{}", candidate.address, template_name);
|
|
}
|
|
|
|
std::string BuildSavestateFilename(std::string_view game_id, int slot)
|
|
{
|
|
return fmt::format("{}{}.s{:02d}", File::GetUserPath(D_STATESAVES_IDX), game_id, slot);
|
|
}
|
|
|
|
std::optional<std::filesystem::file_time_type> GetLastWriteTime(const std::string& path)
|
|
{
|
|
std::error_code error;
|
|
const auto timestamp = std::filesystem::last_write_time(StringToPath(path), error);
|
|
if (error)
|
|
return std::nullopt;
|
|
return timestamp;
|
|
}
|
|
|
|
size_t FindCandidateIndexByAddress(const std::vector<CullingCodeFinder::Candidate>& candidates, u32 address)
|
|
{
|
|
const auto it = std::ranges::find(candidates, address, &CullingCodeFinder::Candidate::address);
|
|
if (it == candidates.end())
|
|
return candidates.size();
|
|
return static_cast<size_t>(std::distance(candidates.begin(), it));
|
|
}
|
|
} // namespace
|
|
|
|
CullingCodeFinder& CullingCodeFinder::GetInstance()
|
|
{
|
|
static CullingCodeFinder instance;
|
|
return instance;
|
|
}
|
|
|
|
void CullingCodeFinder::OnFrameEnd()
|
|
{
|
|
std::lock_guard lock(m_stats_mutex);
|
|
m_display_stats.draw_calls = g_stats.this_frame.num_draw_calls;
|
|
m_display_stats.triangles_drawn = g_stats.this_frame.num_triangles_drawn;
|
|
m_display_stats.prims = g_stats.this_frame.num_prims;
|
|
m_display_stats.drawn_objects = g_stats.this_frame.num_drawn_objects;
|
|
m_frame_counter.fetch_add(1, std::memory_order_release);
|
|
}
|
|
|
|
CullingCodeFinder::FrameStats CullingCodeFinder::GetLatestStats() const
|
|
{
|
|
std::lock_guard lock(m_stats_mutex);
|
|
return m_display_stats;
|
|
}
|
|
|
|
CullingCodeFinder::FrameStats CullingCodeFinder::GetBaseline() const
|
|
{
|
|
return m_baseline;
|
|
}
|
|
|
|
void CullingCodeFinder::SetBaseline()
|
|
{
|
|
std::lock_guard lock(m_stats_mutex);
|
|
m_baseline = m_display_stats;
|
|
m_baseline_set = true;
|
|
}
|
|
|
|
bool CullingCodeFinder::StartScan(Core::System& system, ScanConfig config, std::string* error)
|
|
{
|
|
if (!m_baseline_set)
|
|
{
|
|
if (error)
|
|
*error = "Set a baseline first.";
|
|
return false;
|
|
}
|
|
|
|
if (config.output_folder.empty())
|
|
config.output_folder = GetDefaultOutputFolder(SConfig::GetInstance().GetGameID());
|
|
|
|
File::CreateFullPath(config.output_folder + DIR_SEP);
|
|
m_scan_config = std::move(config);
|
|
m_game_id = SConfig::GetInstance().GetGameID();
|
|
m_manifest_path = PathToString(StringToPath(m_scan_config.output_folder) / "manifest.ini");
|
|
|
|
m_state = ScanState::Idle;
|
|
m_phase = ScanPhase::None;
|
|
m_status_text = "Preparing scan...";
|
|
m_settle_samples.clear();
|
|
m_load_completed = false;
|
|
m_instruction_patched = false;
|
|
m_saved_entry_instruction = 0;
|
|
m_saved_next_instruction = 0;
|
|
m_patched_address = 0;
|
|
m_runtime_overrides_active = false;
|
|
m_turbo_override_active = false;
|
|
m_audio_mute_override_active = false;
|
|
m_has_last_tested_position = false;
|
|
m_last_tested_address = 0;
|
|
m_last_tested_template_index = 0;
|
|
|
|
if (TryResumeFromManifest(system, m_scan_config, error))
|
|
{
|
|
if (m_state != ScanState::Finished)
|
|
{
|
|
m_state = ScanState::Scanning;
|
|
m_phase = ScanPhase::RequestLoad;
|
|
ApplyRuntimeOverrides(system);
|
|
}
|
|
UpdateStatusText();
|
|
return true;
|
|
}
|
|
|
|
return StartFreshScan(system, m_scan_config, error);
|
|
}
|
|
|
|
bool CullingCodeFinder::RestoreSession(Core::System& system, std::string_view output_folder,
|
|
std::string* error, std::string* restored_message)
|
|
{
|
|
const std::string manifest_path =
|
|
PathToString(StringToPath(std::string(output_folder)) / "manifest.ini");
|
|
if (!File::Exists(manifest_path))
|
|
{
|
|
if (error)
|
|
*error = "No manifest.ini was found in the selected output folder.";
|
|
return false;
|
|
}
|
|
|
|
ManifestData manifest;
|
|
if (!LoadManifest(manifest_path, &manifest, error))
|
|
return false;
|
|
|
|
const std::string current_game_id = SConfig::GetInstance().GetGameID();
|
|
if (current_game_id.empty())
|
|
{
|
|
if (error)
|
|
*error = "Boot the game before restoring a culling scan session.";
|
|
return false;
|
|
}
|
|
|
|
if (manifest.game_id != current_game_id)
|
|
{
|
|
if (error)
|
|
{
|
|
*error = fmt::format("The saved session is for {} but the running game is {}.",
|
|
manifest.game_id, current_game_id);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (manifest.candidates.empty())
|
|
{
|
|
if (error)
|
|
*error = "The saved session does not contain any candidates.";
|
|
return false;
|
|
}
|
|
|
|
if (manifest.state == ScanState::Finished || manifest.state == ScanState::Idle)
|
|
{
|
|
if (error)
|
|
*error = "The saved session is not restorable because it is already complete or idle.";
|
|
return false;
|
|
}
|
|
|
|
if (!manifest.baseline_set)
|
|
{
|
|
if (error)
|
|
*error = "The saved session has no baseline statistics.";
|
|
return false;
|
|
}
|
|
|
|
if (manifest.config.output_folder.empty())
|
|
manifest.config.output_folder = std::string(output_folder);
|
|
|
|
size_t candidate_index = manifest.next_candidate_index;
|
|
size_t template_index = TEMPLATE_COUNT;
|
|
for (size_t i = manifest.next_template_index; i < TEMPLATE_COUNT; ++i)
|
|
{
|
|
if (manifest.config.enabled_templates[i])
|
|
{
|
|
template_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (candidate_index >= manifest.candidates.size() || template_index >= TEMPLATE_COUNT)
|
|
{
|
|
if (manifest.has_last_tested_position)
|
|
{
|
|
candidate_index =
|
|
FindCandidateIndexByAddress(manifest.candidates, manifest.last_tested_address);
|
|
template_index = manifest.last_tested_template_index;
|
|
}
|
|
|
|
if (candidate_index >= manifest.candidates.size() || template_index >= TEMPLATE_COUNT ||
|
|
!manifest.config.enabled_templates[template_index])
|
|
{
|
|
if (error)
|
|
*error = "The saved session does not have a valid next test to restore.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const Candidate skipped_candidate = manifest.candidates[candidate_index];
|
|
const PatchTemplateKind skipped_template = TEMPLATE_ORDER[template_index];
|
|
if (!AdvancePositionAfterCrash(manifest.config, manifest.candidates.size(), &candidate_index,
|
|
&template_index))
|
|
{
|
|
if (error)
|
|
{
|
|
*error = fmt::format(
|
|
"The saved session only had {:08X} ({}) remaining, so there is nothing to resume after "
|
|
"skipping it.",
|
|
skipped_candidate.address, GetPatchTemplateName(skipped_template));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
m_scan_config = manifest.config;
|
|
m_game_id = manifest.game_id;
|
|
m_map_path = manifest.map_path;
|
|
m_manifest_path = manifest_path;
|
|
m_baseline = manifest.baseline;
|
|
m_baseline_set = manifest.baseline_set;
|
|
m_candidates = std::move(manifest.candidates);
|
|
m_results = std::move(manifest.results);
|
|
m_candidate_index = candidate_index;
|
|
m_template_index = template_index;
|
|
m_has_last_tested_position = manifest.has_last_tested_position;
|
|
m_last_tested_address = manifest.last_tested_address;
|
|
m_last_tested_template_index = manifest.last_tested_template_index;
|
|
m_state = ScanState::Scanning;
|
|
m_phase = ScanPhase::RequestLoad;
|
|
m_status_text = fmt::format("Restored session after skipping {:08X} ({})",
|
|
skipped_candidate.address, GetPatchTemplateName(skipped_template));
|
|
m_settle_samples.clear();
|
|
m_load_completed = false;
|
|
m_instruction_patched = false;
|
|
m_saved_entry_instruction = 0;
|
|
m_saved_next_instruction = 0;
|
|
m_patched_address = 0;
|
|
m_runtime_overrides_active = false;
|
|
m_turbo_override_active = false;
|
|
m_audio_mute_override_active = false;
|
|
|
|
ApplyRuntimeOverrides(system);
|
|
if (!WriteManifest(error))
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Error;
|
|
if (error && !error->empty())
|
|
m_status_text = *error;
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
UpdateStatusText();
|
|
if (restored_message)
|
|
{
|
|
*restored_message =
|
|
fmt::format("Skipped {:08X} ({}) and resumed from the next pending test.",
|
|
skipped_candidate.address, GetPatchTemplateName(skipped_template));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CullingCodeFinder::PauseScan()
|
|
{
|
|
if (m_state == ScanState::Scanning)
|
|
m_state = ScanState::Paused;
|
|
}
|
|
|
|
void CullingCodeFinder::ResumeScan()
|
|
{
|
|
if (m_state == ScanState::Paused)
|
|
m_state = ScanState::Scanning;
|
|
}
|
|
|
|
void CullingCodeFinder::StopScan()
|
|
{
|
|
m_state = ScanState::Stopped;
|
|
m_phase = ScanPhase::None;
|
|
UpdateStatusText();
|
|
}
|
|
|
|
void CullingCodeFinder::ClearResults(std::string* error)
|
|
{
|
|
m_results.clear();
|
|
if (!m_manifest_path.empty() && File::Exists(m_manifest_path))
|
|
WriteManifest(error);
|
|
}
|
|
|
|
void CullingCodeFinder::NotifySavestateLoaded()
|
|
{
|
|
m_load_completed = true;
|
|
}
|
|
|
|
bool CullingCodeFinder::GenerateSymbolMap(Core::System& system, std::string* error)
|
|
{
|
|
if (SConfig::GetInstance().GetGameID().empty())
|
|
{
|
|
if (error)
|
|
*error = "Boot a game before generating a symbol map.";
|
|
return false;
|
|
}
|
|
|
|
std::string existing_map_path;
|
|
std::string writable_map_path;
|
|
PPCSymbolDB::FindMapFile(&existing_map_path, &writable_map_path);
|
|
if (writable_map_path.empty())
|
|
{
|
|
if (error)
|
|
*error = "Unable to determine the writable map path for the current title.";
|
|
return false;
|
|
}
|
|
|
|
auto& memory = system.GetMemory();
|
|
PPCSymbolDB& symbol_db = system.GetPPCSymbolDB();
|
|
const Core::CPUThreadGuard guard(system);
|
|
|
|
symbol_db.Clear();
|
|
PPCAnalyst::FindFunctions(guard, Memory::MEM1_BASE_ADDR,
|
|
Memory::MEM1_BASE_ADDR + memory.GetRamSizeReal(), &symbol_db);
|
|
|
|
SignatureDB db(SignatureDB::HandlerType::DSY);
|
|
db.Load(File::GetSysDirectory() + TOTALDB);
|
|
db.Apply(guard, &symbol_db);
|
|
|
|
File::CreateFullPath(writable_map_path);
|
|
if (!symbol_db.SaveSymbolMap(writable_map_path))
|
|
{
|
|
if (error)
|
|
*error = fmt::format("Failed to save symbol map to {}", writable_map_path);
|
|
return false;
|
|
}
|
|
|
|
m_map_path = writable_map_path;
|
|
return true;
|
|
}
|
|
|
|
CullingCodeFinder::ScanState CullingCodeFinder::GetState() const
|
|
{
|
|
return m_state;
|
|
}
|
|
|
|
float CullingCodeFinder::GetProgress() const
|
|
{
|
|
const int enabled_templates = GetEnabledTemplateCount();
|
|
if (enabled_templates == 0 || m_candidates.empty())
|
|
return 0.0f;
|
|
|
|
const size_t total_tests = m_candidates.size() * static_cast<size_t>(enabled_templates);
|
|
if (total_tests == 0)
|
|
return 0.0f;
|
|
|
|
return static_cast<float>(ComputeCompletedTests()) / static_cast<float>(total_tests);
|
|
}
|
|
|
|
int CullingCodeFinder::GetResultCount() const
|
|
{
|
|
return static_cast<int>(m_results.size());
|
|
}
|
|
|
|
int CullingCodeFinder::GetCandidateCount() const
|
|
{
|
|
return static_cast<int>(m_candidates.size());
|
|
}
|
|
|
|
std::string CullingCodeFinder::GetStatusText() const
|
|
{
|
|
return m_status_text;
|
|
}
|
|
|
|
std::string CullingCodeFinder::GetMapPath() const
|
|
{
|
|
return m_map_path;
|
|
}
|
|
|
|
CullingCodeFinder::ScanConfig CullingCodeFinder::GetConfig() const
|
|
{
|
|
return m_scan_config;
|
|
}
|
|
|
|
std::vector<CullingCodeFinder::ScanResult> CullingCodeFinder::GetResults() const
|
|
{
|
|
return m_results;
|
|
}
|
|
|
|
bool CullingCodeFinder::IsTurboRuntimeOverrideActive() const
|
|
{
|
|
return m_turbo_override_active;
|
|
}
|
|
|
|
bool CullingCodeFinder::IsAudioMuteRuntimeOverrideActive() const
|
|
{
|
|
return m_audio_mute_override_active;
|
|
}
|
|
|
|
std::string CullingCodeFinder::GetPatchTemplateName(PatchTemplateKind kind)
|
|
{
|
|
return LookupTemplate(kind).display_name;
|
|
}
|
|
|
|
std::array<std::string, 2> CullingCodeFinder::BuildARCodeLines(u32 address, PatchTemplateKind kind)
|
|
{
|
|
const PatchTemplate& patch_template = LookupTemplate(kind);
|
|
const u32 ar_address = 0x04000000u | (address & 0x01FFFFFFu);
|
|
return {fmt::format("{:08X} {:08X}", ar_address, patch_template.first_instruction),
|
|
fmt::format("{:08X} {:08X}", ar_address + 4, BLR_INSTRUCTION)};
|
|
}
|
|
|
|
std::string CullingCodeFinder::BuildARCodeText(u32 address, PatchTemplateKind kind)
|
|
{
|
|
const auto lines = BuildARCodeLines(address, kind);
|
|
return fmt::format("{}\n{}", lines[0], lines[1]);
|
|
}
|
|
|
|
std::string CullingCodeFinder::GetDefaultOutputFolder(std::string_view game_id)
|
|
{
|
|
const std::filesystem::path base = StringToPath(File::GetUserPath(D_SCREENSHOTS_IDX));
|
|
return PathToString(base / std::string(game_id) / "CullingFinder");
|
|
}
|
|
|
|
std::vector<CullingCodeFinder::Candidate> CullingCodeFinder::BuildCandidatesFromSymbolInfo(
|
|
std::vector<CandidateSource> sources, std::string_view symbol_filter, int candidate_limit,
|
|
u32 start_address, u32 end_address)
|
|
{
|
|
std::vector<Candidate> candidates;
|
|
candidates.reserve(sources.size());
|
|
|
|
for (const CandidateSource& source : sources)
|
|
{
|
|
if (!source.is_function || !source.valid_entry || !source.valid_stub)
|
|
continue;
|
|
if (!symbol_filter.empty() &&
|
|
!Common::CaseInsensitiveContains(source.symbol_name, symbol_filter))
|
|
{
|
|
continue;
|
|
}
|
|
if (start_address != 0 && source.address < start_address)
|
|
continue;
|
|
if (end_address != 0 && source.address > end_address)
|
|
continue;
|
|
|
|
candidates.push_back({source.symbol_name, source.address});
|
|
}
|
|
|
|
std::ranges::stable_sort(candidates, [](const Candidate& a, const Candidate& b) {
|
|
if (a.address != b.address)
|
|
return a.address < b.address;
|
|
return a.symbol_name < b.symbol_name;
|
|
});
|
|
|
|
if (candidate_limit > 0 && static_cast<int>(candidates.size()) > candidate_limit)
|
|
candidates.resize(static_cast<size_t>(candidate_limit));
|
|
|
|
return candidates;
|
|
}
|
|
|
|
bool CullingCodeFinder::SaveManifest(const std::string& manifest_path, const ManifestData& manifest,
|
|
std::string* error)
|
|
{
|
|
Common::IniFile ini;
|
|
auto* session = ini.GetOrCreateSection(MANIFEST_SECTION_SESSION);
|
|
session->Set("GameId", manifest.game_id);
|
|
session->Set("MapPath", manifest.map_path);
|
|
session->Set("State", StateToString(manifest.state));
|
|
session->Set("SavestateSlot", manifest.config.savestate_slot);
|
|
session->Set("SettleFrames", manifest.config.settle_frames);
|
|
session->Set("MinDrawCallIncrease", manifest.config.min_draw_call_increase);
|
|
session->Set("MinTriangleIncrease", manifest.config.min_triangle_increase);
|
|
session->Set("MinObjectIncrease", manifest.config.min_object_increase);
|
|
session->Set("TimeoutSeconds", manifest.config.timeout_seconds);
|
|
session->Set("CandidateLimit", manifest.config.candidate_limit);
|
|
session->Set("StartAddress", manifest.config.start_address);
|
|
session->Set("EndAddress", manifest.config.end_address);
|
|
session->Set("TurboDuringScan", manifest.config.turbo_during_scan);
|
|
session->Set("MuteAudioDuringScan", manifest.config.mute_audio_during_scan);
|
|
session->Set("AutoCaptureReferenceState", manifest.config.auto_capture_reference_state);
|
|
session->Set("SymbolFilter", manifest.config.symbol_filter);
|
|
session->Set("OutputFolder", manifest.config.output_folder);
|
|
session->Set("TemplateMask", MakeTemplateMask(manifest.config.enabled_templates));
|
|
session->Set("BaselineSet", manifest.baseline_set);
|
|
session->Set("NextCandidateIndex", static_cast<u64>(manifest.next_candidate_index));
|
|
session->Set("NextTemplateIndex", static_cast<u64>(manifest.next_template_index));
|
|
session->Set("HasLastTestedPosition", manifest.has_last_tested_position);
|
|
session->Set("LastTestedAddress", manifest.last_tested_address);
|
|
session->Set("LastTestedTemplateIndex", static_cast<u64>(manifest.last_tested_template_index));
|
|
session->Set("CandidateCount", static_cast<u64>(manifest.candidates.size()));
|
|
session->Set("ResultCount", static_cast<u64>(manifest.results.size()));
|
|
|
|
auto* baseline = ini.GetOrCreateSection(MANIFEST_SECTION_BASELINE);
|
|
baseline->Set("DrawCalls", manifest.baseline.draw_calls);
|
|
baseline->Set("Triangles", manifest.baseline.triangles_drawn);
|
|
baseline->Set("Prims", manifest.baseline.prims);
|
|
baseline->Set("Objects", manifest.baseline.drawn_objects);
|
|
|
|
for (size_t i = 0; i < manifest.candidates.size(); ++i)
|
|
{
|
|
auto* section = ini.GetOrCreateSection(MakeSectionName(MANIFEST_PREFIX_CANDIDATE, i));
|
|
section->Set("Name", manifest.candidates[i].symbol_name);
|
|
section->Set("Address", manifest.candidates[i].address);
|
|
}
|
|
|
|
for (size_t i = 0; i < manifest.results.size(); ++i)
|
|
{
|
|
const ScanResult& result = manifest.results[i];
|
|
auto* section = ini.GetOrCreateSection(MakeSectionName(MANIFEST_PREFIX_RESULT, i));
|
|
section->Set("Name", result.symbol_name);
|
|
section->Set("Address", result.address);
|
|
section->Set("Template", static_cast<int>(result.template_kind));
|
|
section->Set("BaselineDrawCalls", result.baseline.draw_calls);
|
|
section->Set("BaselineTriangles", result.baseline.triangles_drawn);
|
|
section->Set("BaselinePrims", result.baseline.prims);
|
|
section->Set("BaselineObjects", result.baseline.drawn_objects);
|
|
section->Set("MeasuredDrawCalls", result.measured.draw_calls);
|
|
section->Set("MeasuredTriangles", result.measured.triangles_drawn);
|
|
section->Set("MeasuredPrims", result.measured.prims);
|
|
section->Set("MeasuredObjects", result.measured.drawn_objects);
|
|
section->Set("DrawCallDelta", result.draw_call_delta);
|
|
section->Set("TriangleDelta", result.triangle_delta);
|
|
section->Set("PrimDelta", result.prim_delta);
|
|
section->Set("ObjectDelta", result.object_delta);
|
|
section->Set("Timeout", result.caused_timeout);
|
|
section->Set("ScreenshotPath", result.screenshot_path);
|
|
const auto lines = BuildARCodeLines(result.address, result.template_kind);
|
|
section->Set("ARLine1", lines[0]);
|
|
section->Set("ARLine2", lines[1]);
|
|
}
|
|
|
|
File::CreateFullPath(manifest_path);
|
|
if (!ini.Save(manifest_path))
|
|
{
|
|
if (error)
|
|
*error = fmt::format("Failed to save manifest to {}", manifest_path);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CullingCodeFinder::LoadManifest(const std::string& manifest_path, ManifestData* manifest,
|
|
std::string* error)
|
|
{
|
|
Common::IniFile ini;
|
|
if (!ini.Load(manifest_path))
|
|
{
|
|
if (error)
|
|
*error = fmt::format("Failed to load manifest from {}", manifest_path);
|
|
return false;
|
|
}
|
|
|
|
const Common::IniFile::Section* session = ini.GetSection(MANIFEST_SECTION_SESSION);
|
|
const Common::IniFile::Section* baseline = ini.GetSection(MANIFEST_SECTION_BASELINE);
|
|
if (!session || !baseline)
|
|
{
|
|
if (error)
|
|
*error = "Manifest is missing required sections.";
|
|
return false;
|
|
}
|
|
|
|
ManifestData loaded;
|
|
session->Get("GameId", &loaded.game_id, std::string{});
|
|
session->Get("MapPath", &loaded.map_path, std::string{});
|
|
std::string state_string;
|
|
session->Get("State", &state_string, std::string{"Idle"});
|
|
loaded.state = StringToState(state_string);
|
|
session->Get("SavestateSlot", &loaded.config.savestate_slot, 1);
|
|
session->Get("SettleFrames", &loaded.config.settle_frames, 5);
|
|
session->Get("MinDrawCallIncrease", &loaded.config.min_draw_call_increase, 1);
|
|
session->Get("MinTriangleIncrease", &loaded.config.min_triangle_increase, 0);
|
|
session->Get("MinObjectIncrease", &loaded.config.min_object_increase, 0);
|
|
session->Get("TimeoutSeconds", &loaded.config.timeout_seconds, 3);
|
|
session->Get("CandidateLimit", &loaded.config.candidate_limit, 0);
|
|
session->Get("StartAddress", &loaded.config.start_address, 0u);
|
|
session->Get("EndAddress", &loaded.config.end_address, 0u);
|
|
session->Get("TurboDuringScan", &loaded.config.turbo_during_scan, true);
|
|
session->Get("MuteAudioDuringScan", &loaded.config.mute_audio_during_scan, true);
|
|
session->Get("AutoCaptureReferenceState", &loaded.config.auto_capture_reference_state, true);
|
|
session->Get("SymbolFilter", &loaded.config.symbol_filter, std::string{});
|
|
session->Get("OutputFolder", &loaded.config.output_folder, std::string{});
|
|
std::string template_mask;
|
|
session->Get("TemplateMask", &template_mask, std::string{});
|
|
loaded.config.enabled_templates = ParseTemplateMask(template_mask);
|
|
session->Get("BaselineSet", &loaded.baseline_set, false);
|
|
u64 next_candidate_index = 0;
|
|
u64 next_template_index = 0;
|
|
u64 last_tested_template_index = 0;
|
|
u64 candidate_count = 0;
|
|
u64 result_count = 0;
|
|
session->Get("NextCandidateIndex", &next_candidate_index, 0ull);
|
|
session->Get("NextTemplateIndex", &next_template_index, 0ull);
|
|
session->Get("HasLastTestedPosition", &loaded.has_last_tested_position, false);
|
|
session->Get("LastTestedAddress", &loaded.last_tested_address, 0u);
|
|
session->Get("LastTestedTemplateIndex", &last_tested_template_index, 0ull);
|
|
session->Get("CandidateCount", &candidate_count, 0ull);
|
|
session->Get("ResultCount", &result_count, 0ull);
|
|
loaded.next_candidate_index = static_cast<size_t>(next_candidate_index);
|
|
loaded.next_template_index = static_cast<size_t>(next_template_index);
|
|
loaded.last_tested_template_index = static_cast<size_t>(last_tested_template_index);
|
|
|
|
baseline->Get("DrawCalls", &loaded.baseline.draw_calls, 0);
|
|
baseline->Get("Triangles", &loaded.baseline.triangles_drawn, 0);
|
|
baseline->Get("Prims", &loaded.baseline.prims, 0);
|
|
baseline->Get("Objects", &loaded.baseline.drawn_objects, 0);
|
|
|
|
loaded.candidates.reserve(static_cast<size_t>(candidate_count));
|
|
for (size_t i = 0; i < static_cast<size_t>(candidate_count); ++i)
|
|
{
|
|
const Common::IniFile::Section* section =
|
|
ini.GetSection(MakeSectionName(MANIFEST_PREFIX_CANDIDATE, i));
|
|
if (!section)
|
|
continue;
|
|
|
|
Candidate candidate;
|
|
section->Get("Name", &candidate.symbol_name, std::string{});
|
|
section->Get("Address", &candidate.address, 0u);
|
|
if (!candidate.symbol_name.empty() && candidate.address != 0)
|
|
loaded.candidates.push_back(std::move(candidate));
|
|
}
|
|
|
|
loaded.results.reserve(static_cast<size_t>(result_count));
|
|
for (size_t i = 0; i < static_cast<size_t>(result_count); ++i)
|
|
{
|
|
const Common::IniFile::Section* section =
|
|
ini.GetSection(MakeSectionName(MANIFEST_PREFIX_RESULT, i));
|
|
if (!section)
|
|
continue;
|
|
|
|
ScanResult result;
|
|
int template_kind = 0;
|
|
std::string ar_line_1;
|
|
std::string ar_line_2;
|
|
section->Get("Name", &result.symbol_name, std::string{});
|
|
section->Get("Address", &result.address, 0u);
|
|
section->Get("Template", &template_kind, 0);
|
|
result.template_kind = static_cast<PatchTemplateKind>(template_kind);
|
|
section->Get("BaselineDrawCalls", &result.baseline.draw_calls, 0);
|
|
section->Get("BaselineTriangles", &result.baseline.triangles_drawn, 0);
|
|
section->Get("BaselinePrims", &result.baseline.prims, 0);
|
|
section->Get("BaselineObjects", &result.baseline.drawn_objects, 0);
|
|
section->Get("MeasuredDrawCalls", &result.measured.draw_calls, 0);
|
|
section->Get("MeasuredTriangles", &result.measured.triangles_drawn, 0);
|
|
section->Get("MeasuredPrims", &result.measured.prims, 0);
|
|
section->Get("MeasuredObjects", &result.measured.drawn_objects, 0);
|
|
section->Get("DrawCallDelta", &result.draw_call_delta, 0);
|
|
section->Get("TriangleDelta", &result.triangle_delta, 0);
|
|
section->Get("PrimDelta", &result.prim_delta, 0);
|
|
section->Get("ObjectDelta", &result.object_delta, 0);
|
|
section->Get("Timeout", &result.caused_timeout, false);
|
|
section->Get("ScreenshotPath", &result.screenshot_path, std::string{});
|
|
section->Get("ARLine1", &ar_line_1, std::string{});
|
|
section->Get("ARLine2", &ar_line_2, std::string{});
|
|
result.ar_code_text = ar_line_1.empty() || ar_line_2.empty() ?
|
|
BuildARCodeText(result.address, result.template_kind) :
|
|
fmt::format("{}\n{}", ar_line_1, ar_line_2);
|
|
loaded.results.push_back(std::move(result));
|
|
}
|
|
|
|
*manifest = std::move(loaded);
|
|
return true;
|
|
}
|
|
|
|
bool CullingCodeFinder::AdvancePositionAfterCrash(const ScanConfig& config, size_t candidate_count,
|
|
size_t* candidate_index, size_t* template_index)
|
|
{
|
|
if (!candidate_index || !template_index)
|
|
return false;
|
|
|
|
const auto find_next_enabled_template = [&config](size_t start_index) {
|
|
for (size_t i = start_index; i < TEMPLATE_COUNT; ++i)
|
|
{
|
|
if (config.enabled_templates[i])
|
|
return i;
|
|
}
|
|
return TEMPLATE_COUNT;
|
|
};
|
|
|
|
const size_t next_template = find_next_enabled_template(*template_index + 1);
|
|
if (next_template < TEMPLATE_COUNT)
|
|
{
|
|
*template_index = next_template;
|
|
return *candidate_index < candidate_count;
|
|
}
|
|
|
|
++(*candidate_index);
|
|
if (*candidate_index >= candidate_count)
|
|
return false;
|
|
|
|
*template_index = find_next_enabled_template(0);
|
|
return *template_index < TEMPLATE_COUNT;
|
|
}
|
|
|
|
bool CullingCodeFinder::BuildCandidatesFromMap(Core::System& system, std::string* error)
|
|
{
|
|
Core::CPUThreadGuard guard(system);
|
|
PPCSymbolDB& symbol_db = system.GetPPCSymbolDB();
|
|
|
|
std::string writable_map_path;
|
|
if (!PPCSymbolDB::FindMapFile(&m_map_path, &writable_map_path))
|
|
{
|
|
if (error)
|
|
*error = "No symbol map file found for the current title.";
|
|
return false;
|
|
}
|
|
|
|
if (!symbol_db.LoadMap(guard, m_map_path))
|
|
{
|
|
if (error)
|
|
*error = fmt::format("Failed to load symbol map {}", m_map_path);
|
|
return false;
|
|
}
|
|
|
|
std::vector<CandidateSource> sources;
|
|
symbol_db.ForEachSymbol([&guard, &sources](const Common::Symbol& symbol) {
|
|
CandidateSource source;
|
|
source.symbol_name = symbol.name;
|
|
source.address = symbol.address;
|
|
source.is_function = symbol.type == Common::Symbol::Type::Function;
|
|
source.valid_entry = PowerPC::MMU::HostIsInstructionRAMAddress(guard, symbol.address);
|
|
source.valid_stub = PowerPC::MMU::HostIsInstructionRAMAddress(guard, symbol.address + 4);
|
|
sources.push_back(std::move(source));
|
|
});
|
|
|
|
m_candidates =
|
|
BuildCandidatesFromSymbolInfo(std::move(sources), m_scan_config.symbol_filter,
|
|
m_scan_config.candidate_limit, m_scan_config.start_address,
|
|
m_scan_config.end_address);
|
|
if (m_candidates.empty())
|
|
{
|
|
if (error)
|
|
*error = "No valid function entrypoints were found in the map.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CullingCodeFinder::TryResumeFromManifest(Core::System& system, const ScanConfig& config,
|
|
std::string* error)
|
|
{
|
|
if (!File::Exists(m_manifest_path))
|
|
return false;
|
|
|
|
ManifestData manifest;
|
|
if (!LoadManifest(m_manifest_path, &manifest, error))
|
|
return false;
|
|
|
|
if (manifest.game_id != SConfig::GetInstance().GetGameID() ||
|
|
manifest.config.savestate_slot != config.savestate_slot ||
|
|
manifest.config.settle_frames != config.settle_frames ||
|
|
manifest.config.min_draw_call_increase != config.min_draw_call_increase ||
|
|
manifest.config.min_triangle_increase != config.min_triangle_increase ||
|
|
manifest.config.min_object_increase != config.min_object_increase ||
|
|
manifest.config.timeout_seconds != config.timeout_seconds ||
|
|
manifest.config.candidate_limit != config.candidate_limit ||
|
|
manifest.config.start_address != config.start_address ||
|
|
manifest.config.end_address != config.end_address ||
|
|
manifest.config.turbo_during_scan != config.turbo_during_scan ||
|
|
manifest.config.mute_audio_during_scan != config.mute_audio_during_scan ||
|
|
manifest.config.auto_capture_reference_state != config.auto_capture_reference_state ||
|
|
manifest.config.symbol_filter != config.symbol_filter ||
|
|
manifest.config.enabled_templates != config.enabled_templates)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (manifest.state == ScanState::Finished || manifest.state == ScanState::Idle ||
|
|
manifest.state == ScanState::Stopped)
|
|
return false;
|
|
|
|
if (manifest.candidates.empty())
|
|
{
|
|
if (error)
|
|
*error = "Saved manifest has no candidates to resume.";
|
|
return false;
|
|
}
|
|
|
|
m_scan_config = manifest.config;
|
|
m_game_id = manifest.game_id;
|
|
m_map_path = manifest.map_path;
|
|
m_baseline = manifest.baseline;
|
|
m_baseline_set = manifest.baseline_set;
|
|
m_candidates = std::move(manifest.candidates);
|
|
m_results = std::move(manifest.results);
|
|
m_candidate_index = manifest.next_candidate_index;
|
|
m_template_index = FindNextEnabledTemplateIndex(manifest.next_template_index);
|
|
m_has_last_tested_position = manifest.has_last_tested_position;
|
|
m_last_tested_address = manifest.last_tested_address;
|
|
m_last_tested_template_index = manifest.last_tested_template_index;
|
|
|
|
if (m_candidate_index >= m_candidates.size())
|
|
{
|
|
m_candidate_index = m_candidates.size();
|
|
m_state = ScanState::Finished;
|
|
m_phase = ScanPhase::None;
|
|
}
|
|
else
|
|
{
|
|
m_state = ScanState::Idle;
|
|
m_phase = ScanPhase::RequestLoad;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CullingCodeFinder::StartFreshScan(Core::System& system, const ScanConfig& config,
|
|
std::string* error)
|
|
{
|
|
m_scan_config = config;
|
|
if (GetEnabledTemplateCount() == 0)
|
|
{
|
|
if (error)
|
|
*error = "Enable at least one patch template.";
|
|
return false;
|
|
}
|
|
|
|
if (!BuildCandidatesFromMap(system, error))
|
|
{
|
|
m_state = ScanState::Error;
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
m_results.clear();
|
|
m_candidate_index = 0;
|
|
m_template_index = FindNextEnabledTemplateIndex(0);
|
|
m_has_last_tested_position = false;
|
|
m_last_tested_address = 0;
|
|
m_last_tested_template_index = 0;
|
|
m_phase = ScanPhase::RequestLoad;
|
|
m_state = ScanState::Scanning;
|
|
ApplyRuntimeOverrides(system);
|
|
|
|
if (m_scan_config.auto_capture_reference_state && !CaptureReferenceSavestate(system, error))
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Error;
|
|
m_status_text = (error && !error->empty()) ? *error : "Failed to capture reference savestate.";
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
if (!WriteManifest(error))
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Error;
|
|
if (error && !error->empty())
|
|
m_status_text = *error;
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
UpdateStatusText();
|
|
return true;
|
|
}
|
|
|
|
bool CullingCodeFinder::WriteManifest(std::string* error) const
|
|
{
|
|
if (m_manifest_path.empty())
|
|
return true;
|
|
|
|
ManifestData manifest;
|
|
manifest.game_id = m_game_id;
|
|
manifest.map_path = m_map_path;
|
|
manifest.config = m_scan_config;
|
|
manifest.baseline = m_baseline;
|
|
manifest.baseline_set = m_baseline_set;
|
|
manifest.candidates = m_candidates;
|
|
manifest.results = m_results;
|
|
manifest.next_candidate_index = m_candidate_index;
|
|
manifest.next_template_index = m_template_index;
|
|
manifest.has_last_tested_position = m_has_last_tested_position;
|
|
manifest.last_tested_address = m_last_tested_address;
|
|
manifest.last_tested_template_index = m_last_tested_template_index;
|
|
manifest.state = m_state;
|
|
return SaveManifest(m_manifest_path, manifest, error);
|
|
}
|
|
|
|
bool CullingCodeFinder::CaptureReferenceSavestate(Core::System& system, std::string* error)
|
|
{
|
|
const std::string slot_path = BuildSavestateFilename(m_game_id, m_scan_config.savestate_slot);
|
|
const bool existed_before = File::Exists(slot_path);
|
|
const auto previous_write_time = GetLastWriteTime(slot_path);
|
|
|
|
m_status_text = "Capturing reference savestate...";
|
|
State::Save(system, m_scan_config.savestate_slot,
|
|
{.compress = false, .wait_for_completion = true});
|
|
|
|
const bool exists_after = File::Exists(slot_path);
|
|
const auto current_write_time = GetLastWriteTime(slot_path);
|
|
const bool write_time_advanced =
|
|
!previous_write_time.has_value() || !current_write_time.has_value() ||
|
|
current_write_time.value() != previous_write_time.value();
|
|
|
|
if (!exists_after || (existed_before && !write_time_advanced))
|
|
{
|
|
if (error)
|
|
{
|
|
*error = fmt::format("Failed to capture the reference savestate into slot {}.",
|
|
m_scan_config.savestate_slot);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CullingCodeFinder::UpdateStatusText()
|
|
{
|
|
switch (m_state)
|
|
{
|
|
case ScanState::Idle:
|
|
m_status_text = "Idle";
|
|
return;
|
|
case ScanState::Paused:
|
|
m_status_text = fmt::format("Paused at {}/{} candidates", m_candidate_index,
|
|
m_candidates.size());
|
|
return;
|
|
case ScanState::Stopped:
|
|
m_status_text = fmt::format("Stopped at {}/{} candidates", m_candidate_index,
|
|
m_candidates.size());
|
|
return;
|
|
case ScanState::Finished:
|
|
m_status_text =
|
|
fmt::format("Scan complete. {} results from {} candidates.", m_results.size(),
|
|
m_candidates.size());
|
|
return;
|
|
case ScanState::Error:
|
|
if (m_status_text.empty())
|
|
m_status_text = "Scan error";
|
|
return;
|
|
case ScanState::Scanning:
|
|
break;
|
|
}
|
|
|
|
const std::string template_name =
|
|
m_candidate_index < m_candidates.size() ? GetPatchTemplateName(TEMPLATE_ORDER[m_template_index]) :
|
|
std::string{};
|
|
const std::string symbol_name =
|
|
m_candidate_index < m_candidates.size() ? m_candidates[m_candidate_index].symbol_name :
|
|
std::string{};
|
|
|
|
switch (m_phase)
|
|
{
|
|
case ScanPhase::RequestLoad:
|
|
m_status_text = fmt::format("Loading savestate for {} ({})", symbol_name, template_name);
|
|
break;
|
|
case ScanPhase::WaitingForLoad:
|
|
m_status_text = fmt::format("Waiting for savestate load for {} ({})", symbol_name,
|
|
template_name);
|
|
break;
|
|
case ScanPhase::ApplyPatch:
|
|
m_status_text = fmt::format("Applying {} to {}", template_name, symbol_name);
|
|
break;
|
|
case ScanPhase::WaitingForSettle:
|
|
m_status_text = fmt::format("Measuring {} ({})", symbol_name, template_name);
|
|
break;
|
|
case ScanPhase::Finalize:
|
|
m_status_text = fmt::format("Finalizing {} ({})", symbol_name, template_name);
|
|
break;
|
|
case ScanPhase::None:
|
|
m_status_text = "Scanning";
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CullingCodeFinder::RestorePatchedInstructions(Core::System& system)
|
|
{
|
|
if (!m_instruction_patched)
|
|
return;
|
|
|
|
Core::CPUThreadGuard guard(system);
|
|
PowerPC::MMU::HostWrite<u32>(guard, m_saved_entry_instruction, m_patched_address);
|
|
PowerPC::MMU::HostWrite<u32>(guard, m_saved_next_instruction, m_patched_address + 4);
|
|
system.GetJitInterface().InvalidateICache(m_patched_address, 8, true);
|
|
m_instruction_patched = false;
|
|
m_saved_entry_instruction = 0;
|
|
m_saved_next_instruction = 0;
|
|
m_patched_address = 0;
|
|
}
|
|
|
|
void CullingCodeFinder::ApplyRuntimeOverrides(Core::System& system)
|
|
{
|
|
if (m_runtime_overrides_active)
|
|
return;
|
|
|
|
if (m_scan_config.turbo_during_scan && !Core::GetIsThrottlerTempDisabled())
|
|
{
|
|
Core::SetIsThrottlerTempDisabled(true);
|
|
m_turbo_override_active = true;
|
|
}
|
|
|
|
if (m_scan_config.mute_audio_during_scan && !Config::Get(Config::MAIN_AUDIO_MUTED))
|
|
{
|
|
Config::SetCurrent(Config::MAIN_AUDIO_MUTED, true);
|
|
AudioCommon::UpdateSoundStream(system);
|
|
m_audio_mute_override_active = true;
|
|
}
|
|
|
|
m_runtime_overrides_active = true;
|
|
}
|
|
|
|
void CullingCodeFinder::RestoreRuntimeOverrides(Core::System& system)
|
|
{
|
|
if (m_turbo_override_active)
|
|
{
|
|
Core::SetIsThrottlerTempDisabled(false);
|
|
m_turbo_override_active = false;
|
|
}
|
|
|
|
if (m_audio_mute_override_active &&
|
|
Config::GetActiveLayerForConfig(Config::MAIN_AUDIO_MUTED) == Config::LayerType::CurrentRun)
|
|
{
|
|
Config::DeleteKey(Config::LayerType::CurrentRun, Config::MAIN_AUDIO_MUTED);
|
|
AudioCommon::UpdateSoundStream(system);
|
|
}
|
|
|
|
m_audio_mute_override_active = false;
|
|
m_runtime_overrides_active = false;
|
|
}
|
|
|
|
void CullingCodeFinder::AdvanceToNextTemplate()
|
|
{
|
|
const size_t next_template = FindNextEnabledTemplateIndex(m_template_index + 1);
|
|
if (next_template < TEMPLATE_COUNT)
|
|
{
|
|
m_template_index = next_template;
|
|
return;
|
|
}
|
|
|
|
++m_candidate_index;
|
|
m_template_index = FindNextEnabledTemplateIndex(0);
|
|
}
|
|
|
|
void CullingCodeFinder::SortResults()
|
|
{
|
|
std::ranges::stable_sort(m_results, [](const ScanResult& a, const ScanResult& b) {
|
|
if (a.caused_timeout != b.caused_timeout)
|
|
return !a.caused_timeout;
|
|
if (a.draw_call_delta != b.draw_call_delta)
|
|
return a.draw_call_delta > b.draw_call_delta;
|
|
if (a.object_delta != b.object_delta)
|
|
return a.object_delta > b.object_delta;
|
|
if (a.triangle_delta != b.triangle_delta)
|
|
return a.triangle_delta > b.triangle_delta;
|
|
return a.address < b.address;
|
|
});
|
|
}
|
|
|
|
CullingCodeFinder::FrameStats CullingCodeFinder::AverageSamples() const
|
|
{
|
|
FrameStats average{};
|
|
if (m_settle_samples.empty())
|
|
return average;
|
|
|
|
for (const FrameStats& sample : m_settle_samples)
|
|
{
|
|
average.draw_calls += sample.draw_calls;
|
|
average.triangles_drawn += sample.triangles_drawn;
|
|
average.prims += sample.prims;
|
|
average.drawn_objects += sample.drawn_objects;
|
|
}
|
|
|
|
const int count = static_cast<int>(m_settle_samples.size());
|
|
average.draw_calls /= count;
|
|
average.triangles_drawn /= count;
|
|
average.prims /= count;
|
|
average.drawn_objects /= count;
|
|
return average;
|
|
}
|
|
|
|
bool CullingCodeFinder::ShouldKeepResult(const FrameStats& measured) const
|
|
{
|
|
return measured.draw_calls - m_baseline.draw_calls >= m_scan_config.min_draw_call_increase &&
|
|
measured.triangles_drawn - m_baseline.triangles_drawn >=
|
|
m_scan_config.min_triangle_increase &&
|
|
measured.drawn_objects - m_baseline.drawn_objects >= m_scan_config.min_object_increase;
|
|
}
|
|
|
|
std::string CullingCodeFinder::BuildResultScreenshotPath(const Candidate& candidate,
|
|
PatchTemplateKind kind) const
|
|
{
|
|
const std::filesystem::path output_dir = StringToPath(m_scan_config.output_folder);
|
|
return PathToString(output_dir / fmt::format("{}.png", ScreenshotBaseName(candidate, kind)));
|
|
}
|
|
|
|
void CullingCodeFinder::AppendResult(ScanResult result)
|
|
{
|
|
if (result.ar_code_text.empty())
|
|
result.ar_code_text = BuildARCodeText(result.address, result.template_kind);
|
|
m_results.push_back(std::move(result));
|
|
SortResults();
|
|
}
|
|
|
|
int CullingCodeFinder::GetEnabledTemplateCount() const
|
|
{
|
|
return static_cast<int>(std::count(m_scan_config.enabled_templates.begin(),
|
|
m_scan_config.enabled_templates.end(), true));
|
|
}
|
|
|
|
size_t CullingCodeFinder::FindNextEnabledTemplateIndex(size_t start_index) const
|
|
{
|
|
for (size_t i = start_index; i < TEMPLATE_COUNT; ++i)
|
|
{
|
|
if (m_scan_config.enabled_templates[i])
|
|
return i;
|
|
}
|
|
|
|
return TEMPLATE_COUNT;
|
|
}
|
|
|
|
size_t CullingCodeFinder::ComputeCompletedTests() const
|
|
{
|
|
size_t completed = 0;
|
|
for (size_t candidate_index = 0; candidate_index < std::min(m_candidate_index, m_candidates.size());
|
|
++candidate_index)
|
|
{
|
|
completed += static_cast<size_t>(GetEnabledTemplateCount());
|
|
}
|
|
|
|
if (m_candidate_index < m_candidates.size())
|
|
{
|
|
for (size_t template_index = 0; template_index < std::min(m_template_index, TEMPLATE_COUNT);
|
|
++template_index)
|
|
{
|
|
if (m_scan_config.enabled_templates[template_index])
|
|
++completed;
|
|
}
|
|
}
|
|
|
|
return completed;
|
|
}
|
|
|
|
bool CullingCodeFinder::AdvanceScan(Core::System& system)
|
|
{
|
|
if (m_state != ScanState::Scanning)
|
|
{
|
|
if (m_instruction_patched)
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
m_phase = ScanPhase::RequestLoad;
|
|
}
|
|
RestoreRuntimeOverrides(system);
|
|
if (m_state == ScanState::Paused || m_state == ScanState::Stopped ||
|
|
m_state == ScanState::Finished || m_state == ScanState::Error)
|
|
WriteManifest();
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
ApplyRuntimeOverrides(system);
|
|
|
|
if (m_candidates.empty() || GetEnabledTemplateCount() == 0)
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Finished;
|
|
WriteManifest();
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
while (m_state == ScanState::Scanning)
|
|
{
|
|
switch (m_phase)
|
|
{
|
|
case ScanPhase::None:
|
|
case ScanPhase::RequestLoad:
|
|
if (m_candidate_index >= m_candidates.size())
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Finished;
|
|
WriteManifest();
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
m_has_last_tested_position = true;
|
|
m_last_tested_address = m_candidates[m_candidate_index].address;
|
|
m_last_tested_template_index = m_template_index;
|
|
WriteManifest();
|
|
m_load_completed = false;
|
|
m_settle_samples.clear();
|
|
m_last_sampled_frame = m_frame_counter.load(std::memory_order_acquire);
|
|
m_phase_started_at = std::chrono::steady_clock::now();
|
|
State::Load(system, m_scan_config.savestate_slot);
|
|
m_phase = ScanPhase::WaitingForLoad;
|
|
UpdateStatusText();
|
|
return true;
|
|
|
|
case ScanPhase::WaitingForLoad:
|
|
{
|
|
if (m_load_completed)
|
|
{
|
|
m_load_completed = false;
|
|
m_phase = ScanPhase::ApplyPatch;
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
|
|
const auto elapsed =
|
|
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() -
|
|
m_phase_started_at)
|
|
.count();
|
|
if (elapsed >= m_scan_config.timeout_seconds)
|
|
{
|
|
ScanResult result;
|
|
const Candidate& candidate = m_candidates[m_candidate_index];
|
|
result.symbol_name = candidate.symbol_name;
|
|
result.address = candidate.address;
|
|
result.template_kind = TEMPLATE_ORDER[m_template_index];
|
|
result.baseline = m_baseline;
|
|
result.caused_timeout = true;
|
|
result.ar_code_text = BuildARCodeText(result.address, result.template_kind);
|
|
AppendResult(std::move(result));
|
|
AdvanceToNextTemplate();
|
|
WriteManifest();
|
|
m_phase = ScanPhase::RequestLoad;
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
|
|
UpdateStatusText();
|
|
return true;
|
|
}
|
|
|
|
case ScanPhase::ApplyPatch:
|
|
{
|
|
const Candidate& candidate = m_candidates[m_candidate_index];
|
|
const PatchTemplateKind template_kind = TEMPLATE_ORDER[m_template_index];
|
|
const PatchTemplate& patch_template = LookupTemplate(template_kind);
|
|
|
|
Core::CPUThreadGuard guard(system);
|
|
const auto entry = PowerPC::MMU::HostTryRead<u32>(guard, candidate.address);
|
|
const auto next = PowerPC::MMU::HostTryRead<u32>(guard, candidate.address + 4);
|
|
if (!entry.has_value() || !next.has_value())
|
|
{
|
|
ScanResult result;
|
|
result.symbol_name = candidate.symbol_name;
|
|
result.address = candidate.address;
|
|
result.template_kind = template_kind;
|
|
result.baseline = m_baseline;
|
|
result.caused_timeout = true;
|
|
result.ar_code_text = BuildARCodeText(result.address, result.template_kind);
|
|
AppendResult(std::move(result));
|
|
AdvanceToNextTemplate();
|
|
WriteManifest();
|
|
m_phase = ScanPhase::RequestLoad;
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
|
|
m_saved_entry_instruction = entry->value;
|
|
m_saved_next_instruction = next->value;
|
|
m_patched_address = candidate.address;
|
|
m_active_template = template_kind;
|
|
PowerPC::MMU::HostWrite<u32>(guard, patch_template.first_instruction, candidate.address);
|
|
PowerPC::MMU::HostWrite<u32>(guard, BLR_INSTRUCTION, candidate.address + 4);
|
|
system.GetJitInterface().InvalidateICache(candidate.address, 8, true);
|
|
m_instruction_patched = true;
|
|
|
|
m_settle_samples.clear();
|
|
m_settle_frame_start = m_frame_counter.load(std::memory_order_acquire);
|
|
m_last_sampled_frame = m_settle_frame_start;
|
|
m_phase_started_at = std::chrono::steady_clock::now();
|
|
m_phase = ScanPhase::WaitingForSettle;
|
|
UpdateStatusText();
|
|
return true;
|
|
}
|
|
|
|
case ScanPhase::WaitingForSettle:
|
|
{
|
|
const int current_frame = m_frame_counter.load(std::memory_order_acquire);
|
|
if (current_frame > m_last_sampled_frame)
|
|
{
|
|
for (int frame = m_last_sampled_frame + 1; frame <= current_frame; ++frame)
|
|
{
|
|
if (frame > m_settle_frame_start)
|
|
m_settle_samples.push_back(GetLatestStats());
|
|
}
|
|
m_last_sampled_frame = current_frame;
|
|
}
|
|
|
|
if (static_cast<int>(m_settle_samples.size()) >= m_scan_config.settle_frames)
|
|
{
|
|
m_phase = ScanPhase::Finalize;
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
|
|
const auto elapsed =
|
|
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() -
|
|
m_phase_started_at)
|
|
.count();
|
|
if (elapsed >= m_scan_config.timeout_seconds)
|
|
{
|
|
m_phase = ScanPhase::Finalize;
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
|
|
UpdateStatusText();
|
|
return true;
|
|
}
|
|
|
|
case ScanPhase::Finalize:
|
|
{
|
|
const Candidate& candidate = m_candidates[m_candidate_index];
|
|
const PatchTemplateKind template_kind = TEMPLATE_ORDER[m_template_index];
|
|
ScanResult result;
|
|
result.symbol_name = candidate.symbol_name;
|
|
result.address = candidate.address;
|
|
result.template_kind = template_kind;
|
|
result.baseline = m_baseline;
|
|
result.ar_code_text = BuildARCodeText(result.address, result.template_kind);
|
|
|
|
const bool timed_out =
|
|
static_cast<int>(m_settle_samples.size()) < m_scan_config.settle_frames;
|
|
if (timed_out)
|
|
{
|
|
result.caused_timeout = true;
|
|
AppendResult(std::move(result));
|
|
}
|
|
else
|
|
{
|
|
result.measured = AverageSamples();
|
|
result.draw_call_delta = result.measured.draw_calls - m_baseline.draw_calls;
|
|
result.triangle_delta = result.measured.triangles_drawn - m_baseline.triangles_drawn;
|
|
result.prim_delta = result.measured.prims - m_baseline.prims;
|
|
result.object_delta = result.measured.drawn_objects - m_baseline.drawn_objects;
|
|
|
|
if (ShouldKeepResult(result.measured))
|
|
{
|
|
const std::string screenshot_path = BuildResultScreenshotPath(candidate, template_kind);
|
|
if (g_frame_dumper)
|
|
{
|
|
File::CreateFullPath(screenshot_path);
|
|
g_frame_dumper->SaveScreenshot(screenshot_path);
|
|
result.screenshot_path = screenshot_path;
|
|
}
|
|
AppendResult(std::move(result));
|
|
}
|
|
}
|
|
|
|
m_settle_samples.clear();
|
|
AdvanceToNextTemplate();
|
|
if (m_candidate_index >= m_candidates.size())
|
|
{
|
|
RestorePatchedInstructions(system);
|
|
RestoreRuntimeOverrides(system);
|
|
m_state = ScanState::Finished;
|
|
m_phase = ScanPhase::None;
|
|
m_has_last_tested_position = false;
|
|
m_last_tested_address = 0;
|
|
m_last_tested_template_index = 0;
|
|
WriteManifest();
|
|
UpdateStatusText();
|
|
return false;
|
|
}
|
|
|
|
m_phase = ScanPhase::RequestLoad;
|
|
WriteManifest();
|
|
UpdateStatusText();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|