Files
thejerf--baijiSteamVR/Source/Core/VideoCommon/ElementsGroupManager.cpp
T

1998 lines
70 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/ElementsGroupManager.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <fstream>
#include <locale>
#include <map>
#include <set>
#include <sstream>
#include <fmt/format.h>
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Core/ConfigLoaders/GameConfigLoader.h"
#include "VideoCommon/RuntimeElementMatcher.h"
namespace
{
std::string GetVRGameSettingsPath(const std::string& game_id)
{
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + game_id + ".ini";
}
std::string GetSysVRGameSettingsPath(const std::string& filename)
{
return File::GetSysDirectory() + GAMESETTINGSVR_DIR DIR_SEP + filename;
}
struct ParsedElementGroupOverrideFile
{
std::vector<ElementsGroupManager::ElementGroupOverride> entries;
bool has_enable_section = false;
std::set<std::string> enabled_names;
};
std::string ReadFileWithoutElementSections(const std::string& path)
{
std::ifstream file(path);
if (!file.is_open())
return {};
std::ostringstream out;
bool skipping = false;
std::string line;
while (std::getline(file, line))
{
std::string trimmed = line;
if (!trimmed.empty() && trimmed.back() == '\r')
trimmed.pop_back();
if (trimmed == "[ElementsGroupOverride_Enable]" || trimmed == "[ElementsGroupOverride]")
{
skipping = true;
continue;
}
if (skipping && !trimmed.empty() && trimmed[0] == '[')
skipping = false;
if (!skipping)
out << line << "\n";
}
return out.str();
}
bool ParseKeyValue(const std::string& line, std::string& key, std::string& value)
{
const auto eq = line.find('=');
if (eq == std::string::npos)
return false;
key = line.substr(0, eq);
value = line.substr(eq + 1);
while (!key.empty() && key.back() == ' ')
key.pop_back();
while (!value.empty() && value.front() == ' ')
value.erase(value.begin());
return true;
}
// Integers may have been written with locale digit-grouping separators by older builds (e.g.
// "1,000,000"). Strip everything but digits and a leading sign so std::stoi reads the full value
// instead of truncating at the first separator. Self-heals overrides saved before the locale fix.
int ParseGroupedInt(const std::string& value)
{
std::string digits;
digits.reserve(value.size());
for (const char c : value)
{
if (c == '-' && digits.empty())
digits.push_back(c);
else if (c >= '0' && c <= '9')
digits.push_back(c);
}
if (digits.empty() || digits == "-")
return 0;
return std::stoi(digits);
}
bool ParseRuntimeElementSignatureField(ShaderHunter::RuntimeElementSignature* signature,
const std::string& key, const std::string& value)
{
if (key == "sig_perspective")
signature->perspective = (value == "1" || value == "true");
else if (key == "sig_persp_hfov")
signature->perspective_hfov_x100 = ParseGroupedInt(value);
else if (key == "sig_persp_vfov")
signature->perspective_vfov_x100 = ParseGroupedInt(value);
else if (key == "sig_persp_near")
signature->perspective_near_x1000 = ParseGroupedInt(value);
else if (key == "sig_persp_far")
signature->perspective_far_x100 = ParseGroupedInt(value);
else if (key == "sig_ortho_left")
signature->ortho_left_x100 = ParseGroupedInt(value);
else if (key == "sig_ortho_right")
signature->ortho_right_x100 = ParseGroupedInt(value);
else if (key == "sig_ortho_top")
signature->ortho_top_x100 = ParseGroupedInt(value);
else if (key == "sig_ortho_bottom")
signature->ortho_bottom_x100 = ParseGroupedInt(value);
else if (key == "sig_use_projection")
signature->use_projection = (value == "1" || value == "true");
else if (key == "sig_use_projection_type")
signature->use_projection_type = (value == "1" || value == "true");
else if (key == "sig_use_layer")
signature->use_layer = (value == "1" || value == "true");
else if (key == "sig_use_viewport")
signature->use_viewport = (value == "1" || value == "true");
else if (key == "sig_use_scissor")
signature->use_scissor = (value == "1" || value == "true");
else if (key == "sig_use_render_state")
signature->use_render_state = (value == "1" || value == "true");
else if (key == "sig_layer")
signature->ortho_layer = ParseGroupedInt(value);
else if (key == "sig_vp_x")
signature->viewport_x = ParseGroupedInt(value);
else if (key == "sig_vp_y")
signature->viewport_y = ParseGroupedInt(value);
else if (key == "sig_vp_w")
signature->viewport_width = ParseGroupedInt(value);
else if (key == "sig_vp_h")
signature->viewport_height = ParseGroupedInt(value);
else if (key == "sig_sc_l")
signature->scissor_left = ParseGroupedInt(value);
else if (key == "sig_sc_t")
signature->scissor_top = ParseGroupedInt(value);
else if (key == "sig_sc_r")
signature->scissor_right = ParseGroupedInt(value);
else if (key == "sig_sc_b")
signature->scissor_bottom = ParseGroupedInt(value);
else if (key == "sig_alpha")
signature->alpha_test_hex = std::strtoul(value.c_str(), nullptr, 16);
else if (key == "sig_ztest")
signature->ztest = (value == "1" || value == "true");
else if (key == "sig_zupdate")
signature->zupdate = (value == "1" || value == "true");
else if (key == "sig_zfunc")
signature->zfunc = ParseGroupedInt(value);
else if (key == "sig_blend_color")
signature->blend_color_update = (value == "1" || value == "true");
else if (key == "sig_blend_alpha")
signature->blend_alpha_update = (value == "1" || value == "true");
else
return false;
signature->valid = true;
return true;
}
void SaveRuntimeElementSignature(std::ostringstream& out,
const ShaderHunter::RuntimeElementSignature& sig,
const std::string& prefix)
{
out << prefix << "sig_perspective=" << (sig.perspective ? 1 : 0) << "\n";
out << prefix << "sig_persp_hfov=" << sig.perspective_hfov_x100 << "\n";
out << prefix << "sig_persp_vfov=" << sig.perspective_vfov_x100 << "\n";
out << prefix << "sig_persp_near=" << sig.perspective_near_x1000 << "\n";
out << prefix << "sig_persp_far=" << sig.perspective_far_x100 << "\n";
out << prefix << "sig_ortho_left=" << sig.ortho_left_x100 << "\n";
out << prefix << "sig_ortho_right=" << sig.ortho_right_x100 << "\n";
out << prefix << "sig_ortho_top=" << sig.ortho_top_x100 << "\n";
out << prefix << "sig_ortho_bottom=" << sig.ortho_bottom_x100 << "\n";
out << prefix << "sig_use_projection=" << (sig.use_projection ? 1 : 0) << "\n";
out << prefix << "sig_use_projection_type=" << (sig.use_projection_type ? 1 : 0) << "\n";
out << prefix << "sig_use_layer=" << (sig.use_layer ? 1 : 0) << "\n";
out << prefix << "sig_use_viewport=" << (sig.use_viewport ? 1 : 0) << "\n";
out << prefix << "sig_use_scissor=" << (sig.use_scissor ? 1 : 0) << "\n";
out << prefix << "sig_use_render_state=" << (sig.use_render_state ? 1 : 0) << "\n";
out << prefix << "sig_layer=" << sig.ortho_layer << "\n";
out << prefix << "sig_vp_x=" << sig.viewport_x << "\n";
out << prefix << "sig_vp_y=" << sig.viewport_y << "\n";
out << prefix << "sig_vp_w=" << sig.viewport_width << "\n";
out << prefix << "sig_vp_h=" << sig.viewport_height << "\n";
out << prefix << "sig_sc_l=" << sig.scissor_left << "\n";
out << prefix << "sig_sc_t=" << sig.scissor_top << "\n";
out << prefix << "sig_sc_r=" << sig.scissor_right << "\n";
out << prefix << "sig_sc_b=" << sig.scissor_bottom << "\n";
out << prefix << "sig_alpha=" << fmt::format("{:08x}", sig.alpha_test_hex) << "\n";
out << prefix << "sig_ztest=" << (sig.ztest ? 1 : 0) << "\n";
out << prefix << "sig_zupdate=" << (sig.zupdate ? 1 : 0) << "\n";
out << prefix << "sig_zfunc=" << sig.zfunc << "\n";
out << prefix << "sig_blend_color=" << (sig.blend_color_update ? 1 : 0) << "\n";
out << prefix << "sig_blend_alpha=" << (sig.blend_alpha_update ? 1 : 0) << "\n";
}
std::vector<u64> ParseTextureHashList(const std::string& value)
{
std::vector<u64> hashes;
std::string token;
auto flush_token = [&]() {
if (token.empty())
return;
bool valid = token.size() <= 16;
for (char c : token)
{
if (!std::isxdigit(static_cast<unsigned char>(c)))
{
valid = false;
break;
}
}
if (valid)
{
const u64 hash = std::strtoull(token.c_str(), nullptr, 16);
if (hash != 0)
hashes.push_back(hash);
}
token.clear();
};
for (char c : value)
{
if (c == ',' || c == ';' || std::isspace(static_cast<unsigned char>(c)))
flush_token();
else
token.push_back(c);
}
flush_token();
std::sort(hashes.begin(), hashes.end());
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
return hashes;
}
std::vector<MetroidElementLayer> ParseProfileLayerList(const std::string& value)
{
std::vector<MetroidElementLayer> layers;
std::string token;
auto flush_token = [&]() {
if (token.empty())
return;
if (const auto layer = MetroidElementLayerFromString(token))
layers.push_back(*layer);
token.clear();
};
for (char c : value)
{
if (c == ',' || c == ';' || std::isspace(static_cast<unsigned char>(c)))
flush_token();
else
token.push_back(c);
}
flush_token();
return layers;
}
void SortDeduplicateProfileLayers(std::vector<MetroidElementLayer>* layers)
{
std::sort(layers->begin(), layers->end(), [](MetroidElementLayer lhs, MetroidElementLayer rhs) {
return static_cast<int>(lhs) < static_cast<int>(rhs);
});
layers->erase(std::unique(layers->begin(), layers->end()), layers->end());
}
std::vector<u64> CollectNonZeroTextureHashes(const std::array<u64, 8>& textures)
{
std::vector<u64> hashes;
hashes.reserve(textures.size());
for (u64 texture_hash : textures)
{
if (texture_hash != 0)
hashes.push_back(texture_hash);
}
std::sort(hashes.begin(), hashes.end());
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
return hashes;
}
void HashCombine(size_t& seed, size_t value)
{
seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template <typename T>
void HashCombineValue(size_t& seed, const T& value)
{
HashCombine(seed, std::hash<T>{}(value));
}
size_t ComputeRuntimeElementKey(const ShaderHunter::RuntimeElementSignature& sig,
const std::vector<u64>& texture_hashes, bool texture_excluded)
{
size_t seed = 0;
HashCombineValue(seed, sig.valid);
HashCombineValue(seed, sig.perspective);
HashCombineValue(seed, sig.use_projection);
HashCombineValue(seed, sig.use_projection_type);
HashCombineValue(seed, sig.use_layer);
HashCombineValue(seed, sig.use_viewport);
HashCombineValue(seed, sig.use_scissor);
HashCombineValue(seed, sig.use_render_state);
HashCombineValue(seed, sig.perspective_hfov_x100);
HashCombineValue(seed, sig.perspective_vfov_x100);
HashCombineValue(seed, sig.perspective_near_x1000);
HashCombineValue(seed, sig.perspective_far_x100);
HashCombineValue(seed, sig.ortho_left_x100);
HashCombineValue(seed, sig.ortho_right_x100);
HashCombineValue(seed, sig.ortho_top_x100);
HashCombineValue(seed, sig.ortho_bottom_x100);
HashCombineValue(seed, sig.ortho_layer);
HashCombineValue(seed, sig.viewport_x);
HashCombineValue(seed, sig.viewport_y);
HashCombineValue(seed, sig.viewport_width);
HashCombineValue(seed, sig.viewport_height);
HashCombineValue(seed, sig.scissor_left);
HashCombineValue(seed, sig.scissor_top);
HashCombineValue(seed, sig.scissor_right);
HashCombineValue(seed, sig.scissor_bottom);
HashCombineValue(seed, sig.alpha_test_hex);
HashCombineValue(seed, sig.ztest);
HashCombineValue(seed, sig.zupdate);
HashCombineValue(seed, sig.zfunc);
HashCombineValue(seed, sig.blend_color_update);
HashCombineValue(seed, sig.blend_alpha_update);
HashCombineValue(seed, texture_excluded);
for (const u64 texture_hash : texture_hashes)
HashCombineValue(seed, texture_hash);
return seed;
}
size_t ComputeProfileElementKey(MetroidElementProfile profile,
const std::vector<MetroidElementLayer>& layers,
const std::vector<u64>& texture_hashes, bool texture_excluded)
{
size_t seed = 0;
HashCombineValue(seed, static_cast<int>(profile));
for (const MetroidElementLayer layer : layers)
HashCombineValue(seed, static_cast<int>(layer));
HashCombineValue(seed, texture_excluded);
for (const u64 texture_hash : texture_hashes)
HashCombineValue(seed, texture_hash);
return seed;
}
const char* GetHandlingName(ElementsGroupManager::HandlingType handling)
{
return handling == ElementsGroupManager::HandlingType::Screen ? "screen" :
handling == ElementsGroupManager::HandlingType::Fullscreen ? "fullscreen" :
handling == ElementsGroupManager::HandlingType::FullscreenMono ? "fullscreen_mono" :
handling == ElementsGroupManager::HandlingType::HeadLocked ? "headlocked" :
handling == ElementsGroupManager::HandlingType::Flag ? "flag" :
handling == ElementsGroupManager::HandlingType::UnitsPerMeter ? "units_per_meter" :
"skip";
}
bool SignaturesEqual(const ShaderHunter::RuntimeElementSignature& lhs,
const ShaderHunter::RuntimeElementSignature& rhs)
{
return lhs.valid == rhs.valid && lhs.perspective == rhs.perspective &&
lhs.perspective_hfov_x100 == rhs.perspective_hfov_x100 &&
lhs.perspective_vfov_x100 == rhs.perspective_vfov_x100 &&
lhs.perspective_near_x1000 == rhs.perspective_near_x1000 &&
lhs.perspective_far_x100 == rhs.perspective_far_x100 &&
lhs.ortho_left_x100 == rhs.ortho_left_x100 &&
lhs.ortho_right_x100 == rhs.ortho_right_x100 &&
lhs.ortho_top_x100 == rhs.ortho_top_x100 &&
lhs.ortho_bottom_x100 == rhs.ortho_bottom_x100 &&
lhs.ortho_layer == rhs.ortho_layer && lhs.viewport_x == rhs.viewport_x &&
lhs.viewport_y == rhs.viewport_y && lhs.viewport_width == rhs.viewport_width &&
lhs.viewport_height == rhs.viewport_height &&
lhs.scissor_left == rhs.scissor_left && lhs.scissor_top == rhs.scissor_top &&
lhs.scissor_right == rhs.scissor_right && lhs.scissor_bottom == rhs.scissor_bottom &&
lhs.alpha_test_hex == rhs.alpha_test_hex && lhs.ztest == rhs.ztest &&
lhs.zupdate == rhs.zupdate && lhs.zfunc == rhs.zfunc &&
lhs.blend_color_update == rhs.blend_color_update &&
lhs.blend_alpha_update == rhs.blend_alpha_update;
}
bool StableSubMatchSignaturesEqual(const ElementsGroupManager::StableSubMatchSignature& lhs,
const ElementsGroupManager::StableSubMatchSignature& rhs)
{
return SignaturesEqual(lhs.runtime_element, rhs.runtime_element) && lhs.vs_family == rhs.vs_family &&
lhs.ps_family == rhs.ps_family && lhs.gs_family == rhs.gs_family &&
lhs.texture_hashes == rhs.texture_hashes && lhs.occurrence_slot == rhs.occurrence_slot;
}
bool SelectedSubgroupSignaturesEqual(const ElementsGroupManager::SelectedSubgroupSignature& lhs,
const ElementsGroupManager::SelectedSubgroupSignature& rhs)
{
return lhs.vs_family == rhs.vs_family && lhs.ps_family == rhs.ps_family &&
lhs.gs_family == rhs.gs_family && lhs.texture_hashes == rhs.texture_hashes;
}
bool StableSubMatchBaseEqual(const ElementsGroupManager::StableSubMatchSignature& lhs,
const ElementsGroupManager::StableSubMatchSignature& rhs)
{
return SignaturesEqual(lhs.runtime_element, rhs.runtime_element) && lhs.vs_family == rhs.vs_family &&
lhs.ps_family == rhs.ps_family && lhs.gs_family == rhs.gs_family &&
lhs.texture_hashes == rhs.texture_hashes;
}
size_t ComputeSelectedSubgroupKey(const ElementsGroupManager::SelectedSubgroupSignature& signature)
{
size_t seed = 0;
HashCombineValue(seed, signature.vs_family);
HashCombineValue(seed, signature.ps_family);
HashCombineValue(seed, signature.gs_family);
for (const u64 texture_hash : signature.texture_hashes)
HashCombineValue(seed, texture_hash);
return seed;
}
size_t ComputeStableSubMatchBaseKey(const ElementsGroupManager::StableSubMatchSignature& signature)
{
size_t seed = ComputeRuntimeElementKey(signature.runtime_element, signature.texture_hashes, false);
HashCombineValue(seed, signature.vs_family);
HashCombineValue(seed, signature.ps_family);
HashCombineValue(seed, signature.gs_family);
return seed;
}
bool GroupMaskHasActiveGroups(bool projection, bool layer, bool viewport, bool scissor,
bool render_state)
{
return projection || layer || viewport || scissor || render_state;
}
void SaveStableSubMatchSignature(std::ostringstream& out,
const ElementsGroupManager::StableSubMatchSignature& signature,
const std::string& prefix)
{
SaveRuntimeElementSignature(out, signature.runtime_element, prefix);
if (signature.vs_family != 0)
out << prefix << "vs_family=" << fmt::format("{:016x}", signature.vs_family) << "\n";
if (signature.ps_family != 0)
out << prefix << "ps_family=" << fmt::format("{:016x}", signature.ps_family) << "\n";
if (signature.gs_family != 0)
out << prefix << "gs_family=" << fmt::format("{:016x}", signature.gs_family) << "\n";
for (u64 texture_hash : signature.texture_hashes)
out << prefix << "texture=" << fmt::format("{:016x}", texture_hash) << "\n";
out << prefix << "slot=" << signature.occurrence_slot << "\n";
}
void SaveSelectedSubgroupSignature(std::ostringstream& out,
const ElementsGroupManager::SelectedSubgroupSignature& signature,
const std::string& prefix)
{
if (signature.vs_family != 0)
out << prefix << "vs_family=" << fmt::format("{:016x}", signature.vs_family) << "\n";
if (signature.ps_family != 0)
out << prefix << "ps_family=" << fmt::format("{:016x}", signature.ps_family) << "\n";
if (signature.gs_family != 0)
out << prefix << "gs_family=" << fmt::format("{:016x}", signature.gs_family) << "\n";
for (u64 texture_hash : signature.texture_hashes)
out << prefix << "texture=" << fmt::format("{:016x}", texture_hash) << "\n";
}
bool ParseStableSubMatchField(ElementsGroupManager::StableSubMatchSignature* signature,
const std::string& key, const std::string& value)
{
if (ParseRuntimeElementSignatureField(&signature->runtime_element, key, value))
return true;
if (key == "vs_family")
signature->vs_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "ps_family")
signature->ps_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "gs_family")
signature->gs_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "texture")
{
const u64 parsed = std::strtoull(value.c_str(), nullptr, 16);
if (parsed != 0)
signature->texture_hashes.push_back(parsed);
}
else if (key == "slot")
signature->occurrence_slot = ParseGroupedInt(value);
else
return false;
signature->runtime_element.valid = true;
return true;
}
bool ParseSelectedSubgroupField(ElementsGroupManager::SelectedSubgroupSignature* signature,
const std::string& key, const std::string& value)
{
if (key == "vs_family")
signature->vs_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "ps_family")
signature->ps_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "gs_family")
signature->gs_family = std::strtoull(value.c_str(), nullptr, 16);
else if (key == "texture")
{
const u64 parsed = std::strtoull(value.c_str(), nullptr, 16);
if (parsed != 0)
signature->texture_hashes.push_back(parsed);
}
else
{
return false;
}
return true;
}
} // namespace
u64 ElementsGroupManager::DrawRecord::GetHash(ShaderType type) const
{
switch (type)
{
case ShaderType::Vertex:
return vs_hash;
case ShaderType::Geometry:
return gs_hash;
case ShaderType::Pixel:
default:
return ps_hash;
}
}
u64 ElementsGroupManager::DrawRecord::GetFamily(ShaderType type) const
{
switch (type)
{
case ShaderType::Vertex:
return vs_family;
case ShaderType::Geometry:
return gs_family;
case ShaderType::Pixel:
default:
return ps_family;
}
}
ElementsGroupManager& ElementsGroupManager::GetInstance()
{
static ElementsGroupManager instance;
return instance;
}
ElementsGroupManager::RuntimeElementSignature
ElementsGroupManager::MakeSelectedMatchFilterSignature(
const RuntimeElementSignature& signature)
{
RuntimeElementSignature filter = signature;
filter.valid = true;
filter.use_projection = true;
filter.use_layer = true;
filter.use_viewport = true;
filter.use_scissor = true;
filter.use_render_state = true;
return filter;
}
ElementsGroupManager::StableSubMatchSignature ElementsGroupManager::MakeStableSubMatchSignature(
const DrawRecord& draw, int occurrence_slot)
{
StableSubMatchSignature signature;
signature.runtime_element = MakeSelectedMatchFilterSignature(draw.signature);
signature.vs_family = draw.vs_family;
signature.ps_family = draw.ps_family;
signature.gs_family = draw.gs_family;
signature.texture_hashes = CollectNonZeroTextureHashes(draw.textures);
signature.occurrence_slot = occurrence_slot;
return signature;
}
ElementsGroupManager::SelectedSubgroupSignature
ElementsGroupManager::MakeSelectedSubgroupSignature(const DrawRecord& draw)
{
SelectedSubgroupSignature signature;
signature.vs_family = draw.vs_family;
signature.ps_family = draw.ps_family;
signature.gs_family = draw.gs_family;
signature.texture_hashes = CollectNonZeroTextureHashes(draw.textures);
return signature;
}
size_t ElementsGroupManager::CounterKeyHasher::operator()(const CounterKey& key) const noexcept
{
return std::hash<u64>{}(key.value);
}
ParsedElementGroupOverrideFile
LoadElementGroupOverridesFromINIFile(const std::string& path)
{
using ElementGroupOverride = ElementsGroupManager::ElementGroupOverride;
using StableSubMatchSignature = ElementsGroupManager::StableSubMatchSignature;
using SelectedSubgroupSignature = ElementsGroupManager::SelectedSubgroupSignature;
using ShaderType = ElementsGroupManager::ShaderType;
using HandlingType = ElementsGroupManager::HandlingType;
ParsedElementGroupOverrideFile parsed;
std::ifstream file(path);
if (!file.is_open())
return parsed;
{
bool in_section = false;
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line == "[ElementsGroupOverride_Enable]")
{
in_section = true;
parsed.has_enable_section = true;
continue;
}
if (in_section && !line.empty() && line[0] == '[')
break;
if (!in_section || line.empty())
continue;
if (line[0] == '$')
parsed.enabled_names.insert(line.substr(1));
}
}
file.clear();
file.seekg(0);
bool in_section = false;
ElementGroupOverride current{};
bool has_entry = false;
std::string current_format;
std::map<int, StableSubMatchSignature> current_selected_match_filters_v3;
std::map<int, SelectedSubgroupSignature> current_selected_match_filters_v4;
auto commit_entry = [&]() {
const bool runtime_format =
current_format == "element_only_v2" || current_format == "element_only_v3" ||
current_format == "element_only_v4" || current_format == "element_only_v5";
const bool profile_format = current_format == "element_profile_v1";
if (!has_entry || current.name.empty() || (!runtime_format && !profile_format))
return;
current.enabled = parsed.has_enable_section ? (parsed.enabled_names.count(current.name) > 0) :
true;
current.selected_match_filter.clear();
if (current_format == "element_only_v3")
{
for (auto& [index, signature] : current_selected_match_filters_v3)
{
std::sort(signature.texture_hashes.begin(), signature.texture_hashes.end());
signature.texture_hashes.erase(
std::unique(signature.texture_hashes.begin(), signature.texture_hashes.end()),
signature.texture_hashes.end());
SelectedSubgroupSignature subgroup;
subgroup.vs_family = signature.vs_family;
subgroup.ps_family = signature.ps_family;
subgroup.gs_family = signature.gs_family;
subgroup.texture_hashes = signature.texture_hashes;
if ((subgroup.vs_family != 0 || subgroup.ps_family != 0 || subgroup.gs_family != 0 ||
!subgroup.texture_hashes.empty()) &&
std::none_of(current.selected_match_filter.begin(), current.selected_match_filter.end(),
[&subgroup](const SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, subgroup);
}))
{
current.selected_match_filter.push_back(subgroup);
}
}
}
else if (current_format == "element_only_v4" || current_format == "element_only_v5")
{
for (auto& [index, signature] : current_selected_match_filters_v4)
{
std::sort(signature.texture_hashes.begin(), signature.texture_hashes.end());
signature.texture_hashes.erase(
std::unique(signature.texture_hashes.begin(), signature.texture_hashes.end()),
signature.texture_hashes.end());
if ((signature.vs_family != 0 || signature.ps_family != 0 || signature.gs_family != 0 ||
!signature.texture_hashes.empty()) &&
std::none_of(current.selected_match_filter.begin(), current.selected_match_filter.end(),
[&signature](const SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, signature);
}))
{
current.selected_match_filter.push_back(signature);
}
}
}
if (profile_format)
{
current.match_kind = ElementsGroupManager::MatchKind::ProfileLayer;
SortDeduplicateProfileLayers(&current.profile_layers);
if (current.profile_id != MetroidElementProfile::None && !current.profile_layers.empty())
parsed.entries.push_back(current);
}
else if (current.runtime_element.valid)
{
current.match_kind = ElementsGroupManager::MatchKind::RuntimeSignature;
parsed.entries.push_back(current);
}
};
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line == "[ElementsGroupOverride]")
{
in_section = true;
continue;
}
if (in_section && !line.empty() && line[0] == '[')
break;
if (!in_section || line.empty())
continue;
if (line[0] == '$')
{
commit_entry();
current = {};
current.name = line.substr(1);
current.handling = HandlingType::Skip;
current.enabled = false;
current.user_defined = true;
has_entry = true;
current_format.clear();
current_selected_match_filters_v3.clear();
current_selected_match_filters_v4.clear();
continue;
}
if (!has_entry)
continue;
std::string key;
std::string value;
if (!ParseKeyValue(line, key, value))
continue;
if (key == "format")
{
current_format = value;
current.match_kind = current_format == "element_profile_v1" ?
ElementsGroupManager::MatchKind::ProfileLayer :
ElementsGroupManager::MatchKind::RuntimeSignature;
}
else if (key == "handling")
current.handling = value == "screen" ? HandlingType::Screen :
value == "fullscreen" ? HandlingType::Fullscreen :
value == "fullscreen_mono" ? HandlingType::FullscreenMono :
value == "headlocked" ? HandlingType::HeadLocked :
value == "flag" ? HandlingType::Flag :
value == "units_per_meter" || value == "upm" ?
HandlingType::UnitsPerMeter :
HandlingType::Skip;
else if (key == "layer")
current.layer = ParseGroupedInt(value);
else if (key == "element_depth")
current.element_depth = std::stof(value);
else if (key == "units_per_meter" || key == "upm")
current.units_per_meter = std::stof(value);
else if (key == "flag")
current.flag_group = value;
else if (key == "condition")
current.condition_flag = value;
else if (key == "condition_mode")
current.condition_inverted =
(value == "deactivate" || value == "inactive" || value == "1" || value == "true");
else if (key == "element_start")
current.element_start = ParseGroupedInt(value);
else if (key == "element_end")
current.element_end = ParseGroupedInt(value);
else if (key == "element_total")
current.element_reference_total = ParseGroupedInt(value);
else if (key == "texture")
{
const auto parsed_hashes = ParseTextureHashList(value);
current.texture_hashes.insert(current.texture_hashes.end(), parsed_hashes.begin(),
parsed_hashes.end());
}
else if (key == "texture_mode")
current.texture_hashes_excluded =
(value == "exclude" || value == "excluded" || value == "not");
else if (key == "selected_match_mode")
current.selected_match_filter_excluded =
(value == "exclude" || value == "excluded" || value == "not");
else if (key == "comments")
current.comments = value;
else if (key == "credits")
current.credits = value;
else if (key == "profile")
{
if (const auto profile = MetroidElementProfileFromString(value))
current.profile_id = *profile;
}
else if (key == "profile_layer" || key == "profile_layers")
{
const auto parsed_layers = ParseProfileLayerList(value);
current.profile_layers.insert(current.profile_layers.end(), parsed_layers.begin(),
parsed_layers.end());
}
else if (current_format == "element_only_v3" && key.rfind("selected_match_", 0) == 0)
{
const size_t prefix_len = std::string("selected_match_").size();
const size_t field_sep = key.find('_', prefix_len);
if (field_sep != std::string::npos)
{
const std::string index_str = key.substr(prefix_len, field_sep - prefix_len);
const std::string subkey = key.substr(field_sep + 1);
const int match_index = std::stoi(index_str);
ParseStableSubMatchField(&current_selected_match_filters_v3[match_index], subkey, value);
}
}
else if ((current_format == "element_only_v4" || current_format == "element_only_v5") &&
key.rfind("selected_match_", 0) == 0)
{
const size_t prefix_len = std::string("selected_match_").size();
const size_t field_sep = key.find('_', prefix_len);
if (field_sep != std::string::npos)
{
const std::string index_str = key.substr(prefix_len, field_sep - prefix_len);
const std::string subkey = key.substr(field_sep + 1);
const int match_index = std::stoi(index_str);
ParseSelectedSubgroupField(&current_selected_match_filters_v4[match_index], subkey, value);
}
}
else if (ParseRuntimeElementSignatureField(&current.runtime_element, key, value))
{
continue;
}
}
commit_entry();
return parsed;
}
static void MergeParsedElementGroupOverrideFile(
std::vector<ElementsGroupManager::ElementGroupOverride>* result,
std::map<std::string, size_t>* index_by_name, ParsedElementGroupOverrideFile parsed)
{
for (auto& entry : parsed.entries)
{
const auto it = index_by_name->find(entry.name);
if (it != index_by_name->end())
(*result)[it->second] = std::move(entry);
else
{
const size_t index = result->size();
index_by_name->emplace(entry.name, index);
result->push_back(std::move(entry));
}
}
if (parsed.has_enable_section)
{
for (auto& entry : *result)
entry.enabled = parsed.enabled_names.count(entry.name) > 0;
}
}
std::vector<ElementsGroupManager::ElementGroupOverride>
ElementsGroupManager::LoadOverridesFromINI(const std::string& game_id, std::optional<u16> revision)
{
if (game_id.empty())
return {};
std::vector<ElementGroupOverride> result;
std::map<std::string, size_t> index_by_name;
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
MergeParsedElementGroupOverrideFile(
&result, &index_by_name, LoadElementGroupOverridesFromINIFile(GetSysVRGameSettingsPath(filename)));
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
MergeParsedElementGroupOverrideFile(
&result, &index_by_name,
LoadElementGroupOverridesFromINIFile(File::GetUserPath(D_GAMESETTINGSVR_IDX) + filename));
return result;
}
void ElementsGroupManager::SaveOverridesToINI(const std::string& game_id,
const std::vector<ElementGroupOverride>& overrides)
{
if (game_id.empty())
return;
const std::string path = GetVRGameSettingsPath(game_id);
File::CreateFullPath(path);
std::string base = ReadFileWithoutElementSections(path);
while (!base.empty() && (base.back() == '\n' || base.back() == '\r' || base.back() == ' '))
base.pop_back();
if (!base.empty())
base += "\n";
std::ostringstream out;
// Dolphin sets the global locale to the user's system locale (UICommon::SetLocale), which can add
// digit-grouping separators (e.g. "1,000,000") to streamed integers. std::stoi then truncates at
// the separator on load, corrupting every value >= 1000. Force the classic locale, like the rest
// of Dolphin's serialization code does.
out.imbue(std::locale::classic());
out << base;
out << "[ElementsGroupOverride_Enable]\n";
for (const auto& entry : overrides)
{
if (entry.enabled)
out << "$" << entry.name << "\n";
}
out << "[ElementsGroupOverride]\n";
for (const auto& entry : overrides)
{
out << "$" << entry.name << "\n";
out << "format="
<< (entry.match_kind == MatchKind::ProfileLayer ? "element_profile_v1" : "element_only_v5")
<< "\n";
out << "handling=" << GetHandlingName(entry.handling) << "\n";
if (entry.layer >= 0)
out << "layer=" << entry.layer << "\n";
if (entry.element_depth >= 0.0f)
out << "element_depth=" << entry.element_depth << "\n";
if (entry.units_per_meter > 0.0f)
out << "units_per_meter=" << entry.units_per_meter << "\n";
if (!entry.flag_group.empty())
out << "flag=" << entry.flag_group << "\n";
if (!entry.condition_flag.empty())
{
out << "condition=" << entry.condition_flag << "\n";
out << "condition_mode=" << (entry.condition_inverted ? "deactivate" : "activate") << "\n";
}
if (entry.element_start >= 0)
out << "element_start=" << entry.element_start << "\n";
if (entry.element_end >= 0)
out << "element_end=" << entry.element_end << "\n";
if (entry.element_reference_total > 0)
out << "element_total=" << entry.element_reference_total << "\n";
if (!entry.comments.empty())
out << "comments=" << entry.comments << "\n";
if (!entry.credits.empty())
out << "credits=" << entry.credits << "\n";
if (!entry.texture_hashes.empty())
{
out << "texture_mode=" << (entry.texture_hashes_excluded ? "exclude" : "include") << "\n";
for (u64 texture_hash : entry.texture_hashes)
out << "texture=" << fmt::format("{:016x}", texture_hash) << "\n";
}
if (!entry.selected_match_filter.empty())
{
out << "selected_match_mode="
<< (entry.selected_match_filter_excluded ? "exclude" : "include") << "\n";
}
if (entry.match_kind == MatchKind::ProfileLayer)
{
out << "profile=" << MetroidElementProfileToININame(entry.profile_id) << "\n";
for (const MetroidElementLayer layer : entry.profile_layers)
out << "profile_layer=" << MetroidElementLayerToININame(layer) << "\n";
}
else
{
const auto& sig = entry.runtime_element;
SaveRuntimeElementSignature(out, sig, "");
for (size_t i = 0; i < entry.selected_match_filter.size(); ++i)
{
SaveSelectedSubgroupSignature(out, entry.selected_match_filter[i],
fmt::format("selected_match_{}_", i));
}
}
out << "\n";
}
std::ofstream outfile(path, std::ios::trunc);
outfile << out.str();
}
void ElementsGroupManager::LoadOverrides(const std::string& game_id)
{
std::lock_guard lock(m_mutex);
m_loaded_game_id = game_id;
m_overrides.clear();
m_draw_counters.clear();
m_draw_totals_prev.clear();
m_current_draw_indices.clear();
m_stable_submatch_occurrence_counters.clear();
m_current_stable_submatch = {};
if (game_id.empty())
{
m_has_overrides.store(false);
return;
}
const auto all_overrides = LoadOverridesFromINI(game_id);
for (const auto& entry : all_overrides)
{
if (entry.enabled)
m_overrides.push_back(entry);
}
m_has_overrides.store(!m_overrides.empty());
if (!m_overrides.empty())
{
INFO_LOG_FMT(VIDEO, "ElementsGroup: loaded {} enabled overrides for {}", m_overrides.size(),
game_id);
}
}
void ElementsGroupManager::LoadOverridesIfNeeded(const std::string& game_id)
{
{
std::lock_guard lock(m_mutex);
if (game_id == m_loaded_game_id)
return;
}
LoadOverrides(game_id);
}
bool ElementsGroupManager::HasOverrides() const
{
return m_has_overrides.load();
}
void ElementsGroupManager::SetPopupOpen(bool open)
{
std::lock_guard lock(m_mutex);
if (open)
{
++m_popup_open_count;
}
else if (m_popup_open_count > 0)
{
--m_popup_open_count;
}
if (m_popup_open_count <= 0)
{
m_popup_open_count = 0;
m_hunt_enabled = false;
m_collecting_draws.clear();
m_display_draws.clear();
m_display_seed_candidates.clear();
m_collecting_matches.clear();
m_display_raw_matches.clear();
m_display_matches.clear();
m_collecting_match_total = 0;
m_display_match_total = 0;
m_collecting_highlighted_draw.reset();
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
m_stable_submatch_occurrence_counters.clear();
m_current_stable_submatch = {};
ClearSeedSelectionLocked();
}
m_popup_open.store(m_popup_open_count > 0);
}
bool ElementsGroupManager::IsPopupOpen() const
{
return m_popup_open.load();
}
void ElementsGroupManager::SetHuntEnabled(bool enabled)
{
std::lock_guard lock(m_mutex);
m_hunt_enabled = enabled;
}
bool ElementsGroupManager::IsHuntEnabled() const
{
std::lock_guard lock(m_mutex);
return m_hunt_enabled;
}
void ElementsGroupManager::SetHuntingOption(HuntingOption option)
{
std::lock_guard lock(m_mutex);
m_hunting_option = option;
}
ElementsGroupManager::HuntingOption ElementsGroupManager::GetHuntingOption() const
{
std::lock_guard lock(m_mutex);
return m_hunting_option;
}
void ElementsGroupManager::ClearSeedSelectionLocked()
{
m_seed_signature = {};
m_seed_group_signature = {};
m_seed_draw.reset();
m_selected_seed_index = -1;
m_selected_match = 0;
m_selected_match_filters.clear();
m_selected_match_filters_excluded = false;
m_display_raw_matches.clear();
m_display_matches.clear();
m_display_match_total = 0;
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
}
ElementsGroupManager::RuntimeElementSignature ElementsGroupManager::GetMaskedSeedSignatureLocked() const
{
RuntimeElementSignature masked = m_seed_signature;
masked.use_projection = m_group_mask.projection;
masked.use_layer = m_group_mask.layer;
masked.use_viewport = m_group_mask.viewport;
masked.use_scissor = m_group_mask.scissor;
masked.use_render_state = m_group_mask.render_state;
return masked;
}
ElementsGroupManager::RuntimeElementSignature
ElementsGroupManager::GetSeedGroupSignatureLocked(const RuntimeElementSignature& signature) const
{
if (!signature.valid)
return {};
if (!GroupMaskHasActiveGroups(m_group_mask.projection, m_group_mask.layer, m_group_mask.viewport,
m_group_mask.scissor, m_group_mask.render_state))
return signature;
RuntimeElementSignature grouped{};
grouped.valid = true;
if (m_group_mask.projection)
{
grouped.perspective = signature.perspective;
if (signature.perspective)
{
grouped.perspective_hfov_x100 = signature.perspective_hfov_x100;
grouped.perspective_vfov_x100 = signature.perspective_vfov_x100;
grouped.perspective_near_x1000 = signature.perspective_near_x1000;
grouped.perspective_far_x100 = signature.perspective_far_x100;
}
else
{
grouped.ortho_left_x100 = signature.ortho_left_x100;
grouped.ortho_right_x100 = signature.ortho_right_x100;
grouped.ortho_top_x100 = signature.ortho_top_x100;
grouped.ortho_bottom_x100 = signature.ortho_bottom_x100;
}
}
if (m_group_mask.layer)
{
grouped.perspective = signature.perspective;
grouped.ortho_layer = signature.ortho_layer;
}
if (m_group_mask.viewport)
{
grouped.viewport_x = signature.viewport_x;
grouped.viewport_y = signature.viewport_y;
grouped.viewport_width = signature.viewport_width;
grouped.viewport_height = signature.viewport_height;
}
if (m_group_mask.scissor)
{
grouped.scissor_left = signature.scissor_left;
grouped.scissor_top = signature.scissor_top;
grouped.scissor_right = signature.scissor_right;
grouped.scissor_bottom = signature.scissor_bottom;
}
if (m_group_mask.render_state)
{
grouped.alpha_test_hex = signature.alpha_test_hex;
grouped.ztest = signature.ztest;
grouped.zupdate = signature.zupdate;
grouped.zfunc = signature.zfunc;
grouped.blend_color_update = signature.blend_color_update;
grouped.blend_alpha_update = signature.blend_alpha_update;
}
return grouped;
}
void ElementsGroupManager::SelectSeedCandidateLocked(int index)
{
if (index < 0 || index >= static_cast<int>(m_display_seed_candidates.size()))
{
ClearSeedSelectionLocked();
return;
}
m_selected_seed_index = index;
m_seed_signature = m_display_seed_candidates[index].representative_draw.signature;
m_seed_group_signature = m_display_seed_candidates[index].group_signature;
m_seed_draw = m_display_seed_candidates[index].representative_draw;
m_selected_match = 0;
m_selected_match_filters.clear();
m_selected_match_filters_excluded = false;
m_collecting_match_total = 0;
m_display_match_total = 0;
m_collecting_highlighted_draw.reset();
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
m_collecting_matches.clear();
m_display_raw_matches.clear();
m_display_matches.clear();
}
void ElementsGroupManager::SelectSeedCandidate(int index)
{
std::lock_guard lock(m_mutex);
SelectSeedCandidateLocked(index);
}
void ElementsGroupManager::SetSeedGroupMask(bool projection, bool layer, bool viewport,
bool scissor, bool render_state)
{
std::lock_guard lock(m_mutex);
m_group_mask.projection = projection;
m_group_mask.layer = layer;
m_group_mask.viewport = viewport;
m_group_mask.scissor = scissor;
m_group_mask.render_state = render_state;
if (m_seed_signature.valid)
m_seed_group_signature = GetSeedGroupSignatureLocked(m_seed_signature);
m_selected_match = 0;
m_selected_match_filters.clear();
m_selected_match_filters_excluded = false;
m_collecting_matches.clear();
m_display_raw_matches.clear();
m_display_matches.clear();
m_collecting_match_total = 0;
m_display_match_total = 0;
m_collecting_highlighted_draw.reset();
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
}
void ElementsGroupManager::SelectMatch(int index)
{
std::lock_guard lock(m_mutex);
if (index < 0 || index >= m_display_match_total)
return;
m_selected_match = index;
if (index < static_cast<int>(m_display_matches.size()))
{
m_display_highlighted_draw = m_display_matches[index].representative_draw;
m_display_highlighted_match_raw_draw_count = m_display_matches[index].raw_draw_count;
}
}
void ElementsGroupManager::NextMatch()
{
std::lock_guard lock(m_mutex);
if (m_display_matches.empty())
return;
if (std::none_of(m_display_matches.begin(), m_display_matches.end(),
[](const CurrentMatchCandidate& candidate) { return candidate.active_this_frame; }))
return;
for (int step = 1; step <= static_cast<int>(m_display_matches.size()); ++step)
{
const int next_index = (m_selected_match + step) % static_cast<int>(m_display_matches.size());
if (!m_display_matches[next_index].active_this_frame)
continue;
m_selected_match = next_index;
m_display_highlighted_draw = m_display_matches[m_selected_match].representative_draw;
m_display_highlighted_match_raw_draw_count = m_display_matches[m_selected_match].raw_draw_count;
return;
}
}
void ElementsGroupManager::PrevMatch()
{
std::lock_guard lock(m_mutex);
if (m_display_matches.empty())
return;
if (std::none_of(m_display_matches.begin(), m_display_matches.end(),
[](const CurrentMatchCandidate& candidate) { return candidate.active_this_frame; }))
return;
for (int step = 1; step <= static_cast<int>(m_display_matches.size()); ++step)
{
int prev_index = m_selected_match - step;
while (prev_index < 0)
prev_index += static_cast<int>(m_display_matches.size());
if (!m_display_matches[prev_index].active_this_frame)
continue;
m_selected_match = prev_index;
m_display_highlighted_draw = m_display_matches[m_selected_match].representative_draw;
m_display_highlighted_match_raw_draw_count = m_display_matches[m_selected_match].raw_draw_count;
return;
}
}
std::optional<ElementsGroupManager::DrawRecord> ElementsGroupManager::GetHighlightedDraw() const
{
std::lock_guard lock(m_mutex);
return m_display_highlighted_draw;
}
std::optional<ElementsGroupManager::DrawRecord> ElementsGroupManager::GetSelectedSeedDraw() const
{
std::lock_guard lock(m_mutex);
return m_seed_draw;
}
std::optional<ElementsGroupManager::DrawRecord>
ElementsGroupManager::GetSelectedCurrentMatchDraw() const
{
std::lock_guard lock(m_mutex);
if (m_selected_match < 0 || m_selected_match >= static_cast<int>(m_display_matches.size()))
return std::nullopt;
return m_display_matches[static_cast<size_t>(m_selected_match)].representative_draw;
}
std::optional<ElementsGroupManager::DrawRecord>
ElementsGroupManager::GetCurrentTextureSourceDraw() const
{
std::lock_guard lock(m_mutex);
if (m_selected_match >= 0 && m_selected_match < static_cast<int>(m_display_matches.size()))
return m_display_matches[static_cast<size_t>(m_selected_match)].representative_draw;
return m_seed_draw;
}
std::vector<ElementsGroupManager::DrawRecord> ElementsGroupManager::GetCurrentTextureSourceDraws() const
{
std::lock_guard lock(m_mutex);
if (m_selected_match_filters.empty())
return m_display_raw_matches;
std::vector<DrawRecord> draws;
draws.reserve(m_display_raw_matches.size());
for (size_t i = 0; i < m_display_raw_matches.size() && i < m_display_raw_match_signatures.size();
++i)
{
const bool included =
MatchesSelectedMatchFilterSignatureLocked(m_display_raw_match_signatures[i]);
if (m_selected_match_filters_excluded ? !included : included)
draws.push_back(m_display_raw_matches[i]);
}
return draws;
}
std::vector<ElementsGroupManager::SeedCandidate> ElementsGroupManager::GetSeedCandidates() const
{
std::lock_guard lock(m_mutex);
return m_display_seed_candidates;
}
std::vector<ElementsGroupManager::CurrentMatchCandidate> ElementsGroupManager::GetCurrentMatches() const
{
std::lock_guard lock(m_mutex);
return m_display_matches;
}
std::vector<ElementsGroupManager::SelectedSubgroupSignature>
ElementsGroupManager::GetSelectedMatchFilters() const
{
std::lock_guard lock(m_mutex);
return m_selected_match_filters;
}
bool ElementsGroupManager::GetSelectedMatchFilterExcluded() const
{
std::lock_guard lock(m_mutex);
return m_selected_match_filters_excluded;
}
std::vector<ElementsGroupManager::CurrentMatchCandidate>
ElementsGroupManager::GetSelectedMatchDisplayDraws() const
{
std::lock_guard lock(m_mutex);
return ResolveSelectedMatchDisplayDrawsLocked();
}
ElementsGroupManager::RuntimeElementSignature ElementsGroupManager::GetSeedSignature() const
{
std::lock_guard lock(m_mutex);
return GetMaskedSeedSignatureLocked();
}
ElementsGroupManager::Status ElementsGroupManager::GetStatus() const
{
std::lock_guard lock(m_mutex);
Status status;
status.popup_open = m_popup_open_count > 0;
status.hunt_enabled = m_hunt_enabled;
status.option = m_hunting_option;
status.seed_valid = m_seed_signature.valid;
status.seed_signature = GetMaskedSeedSignatureLocked();
status.seed_draw = m_seed_draw;
status.selected_seed_index = m_selected_seed_index;
status.total_seed_candidates = static_cast<int>(m_display_seed_candidates.size());
status.selected_match = m_selected_match;
status.total_matches = m_display_match_total;
status.active_matches = static_cast<int>(std::count_if(
m_display_matches.begin(), m_display_matches.end(),
[](const CurrentMatchCandidate& candidate) { return candidate.active_this_frame; }));
status.selected_match_filter_count = static_cast<int>(m_selected_match_filters.size());
status.selected_match_filter_excluded = m_selected_match_filters_excluded;
status.highlighted_match_raw_draw_count = m_display_highlighted_match_raw_draw_count;
status.highlighted_draw = m_display_highlighted_draw;
return status;
}
void ElementsGroupManager::SetSelectedMatchFilterExcluded(bool excluded)
{
std::lock_guard lock(m_mutex);
m_selected_match_filters_excluded = excluded;
}
bool ElementsGroupManager::IsCurrentMatchFilterEnabled(int match_index) const
{
std::lock_guard lock(m_mutex);
if (match_index < 0 || match_index >= static_cast<int>(m_display_matches.size()))
return false;
const SelectedSubgroupSignature& filter = m_display_matches[match_index].subgroup;
return std::any_of(m_selected_match_filters.begin(), m_selected_match_filters.end(),
[&filter](const SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, filter);
});
}
void ElementsGroupManager::SetCurrentMatchFilterEnabled(int match_index, bool enabled)
{
std::lock_guard lock(m_mutex);
if (match_index < 0 || match_index >= static_cast<int>(m_display_matches.size()))
return;
const SelectedSubgroupSignature& filter = m_display_matches[match_index].subgroup;
const auto it = std::find_if(m_selected_match_filters.begin(), m_selected_match_filters.end(),
[&filter](const SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, filter);
});
if (enabled)
{
if (it == m_selected_match_filters.end())
m_selected_match_filters.push_back(filter);
}
else if (it != m_selected_match_filters.end())
{
m_selected_match_filters.erase(it);
}
}
void ElementsGroupManager::AddCurrentMatchFilter()
{
std::lock_guard lock(m_mutex);
if (m_selected_match < 0 || m_selected_match >= static_cast<int>(m_display_matches.size()))
return;
const SelectedSubgroupSignature& filter = m_display_matches[m_selected_match].subgroup;
const auto it = std::find_if(m_selected_match_filters.begin(), m_selected_match_filters.end(),
[&filter](const SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, filter);
});
if (it == m_selected_match_filters.end())
m_selected_match_filters.push_back(filter);
}
void ElementsGroupManager::RemoveSelectedMatchFilter(int index)
{
std::lock_guard lock(m_mutex);
if (index < 0 || index >= static_cast<int>(m_selected_match_filters.size()))
return;
m_selected_match_filters.erase(m_selected_match_filters.begin() + index);
}
ElementsGroupManager::PreviewAction ElementsGroupManager::RegisterDraw(const DrawRecord& draw)
{
std::lock_guard lock(m_mutex);
// Seed-candidate evaluation only runs while the window is open AND Group Hunt is enabled,
// so nothing is collected (and no per-draw overhead is paid) until the box is checked.
if (m_popup_open_count > 0 && m_hunt_enabled)
{
DrawRecord recorded = draw;
recorded.draw_index = static_cast<int>(m_collecting_draws.size());
m_collecting_draws.push_back(recorded);
if (HasActiveHuntLocked() &&
RuntimeElementMatcher::Matches(GetMaskedSeedSignatureLocked(), recorded.signature))
{
m_collecting_matches.push_back(recorded);
m_collecting_match_total++;
if (MatchesSelectedMatchFilterLocked(recorded))
return m_hunting_option == HuntingOption::Pink ? PreviewAction::Pink : PreviewAction::Skip;
}
}
return PreviewAction::None;
}
void ElementsGroupManager::OnFrameEnd()
{
std::lock_guard lock(m_mutex);
m_draw_totals_prev = m_draw_counters;
m_draw_counters.clear();
m_current_draw_indices.clear();
if (m_popup_open_count <= 0)
return;
if (!m_hunt_enabled)
{
// Group Hunt is off: drop anything collected and clear the displayed seed/match lists so
// stale candidates don't linger after the box is unchecked. The selected seed signature and
// group mask are preserved so re-enabling resumes the hunt where it left off.
m_collecting_draws.clear();
m_display_draws.clear();
m_display_seed_candidates.clear();
m_collecting_matches.clear();
m_display_raw_matches.clear();
m_display_raw_match_signatures.clear();
m_display_matches.clear();
m_collecting_match_total = 0;
m_display_match_total = 0;
m_collecting_highlighted_draw.reset();
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
return;
}
m_display_draws = std::move(m_collecting_draws);
m_collecting_draws.clear();
m_display_raw_matches = std::move(m_collecting_matches);
m_collecting_matches.clear();
m_display_raw_match_signatures.clear();
for (auto& candidate : m_display_matches)
{
candidate.active_this_frame = false;
candidate.raw_draw_count = 0;
}
std::vector<SelectedSubgroupSignature> raw_signatures;
raw_signatures.reserve(m_display_raw_matches.size());
for (const DrawRecord& draw : m_display_raw_matches)
{
SelectedSubgroupSignature signature = MakeSelectedSubgroupSignature(draw);
raw_signatures.push_back(signature);
}
m_display_raw_match_signatures = raw_signatures;
for (size_t i = 0; i < m_display_raw_matches.size(); ++i)
{
const DrawRecord& draw = m_display_raw_matches[i];
const SelectedSubgroupSignature& signature = raw_signatures[i];
auto candidate_it = std::find_if(m_display_matches.begin(), m_display_matches.end(),
[&signature](const CurrentMatchCandidate& candidate) {
return SelectedSubgroupSignaturesEqual(candidate.subgroup,
signature);
});
if (candidate_it == m_display_matches.end())
m_display_matches.push_back(CurrentMatchCandidate{.subgroup = signature,
.representative_draw = draw,
.raw_draw_count = 1,
.active_this_frame = true});
else
{
candidate_it->representative_draw = draw;
candidate_it->raw_draw_count++;
candidate_it->active_this_frame = true;
}
}
m_display_match_total = static_cast<int>(m_display_matches.size());
m_collecting_match_total = 0;
m_display_seed_candidates.clear();
m_display_seed_candidates.reserve(m_display_draws.size());
for (const DrawRecord& draw : m_display_draws)
{
const RuntimeElementSignature group_signature = GetSeedGroupSignatureLocked(draw.signature);
auto it = std::find_if(m_display_seed_candidates.begin(), m_display_seed_candidates.end(),
[&group_signature](const SeedCandidate& candidate) {
return SignaturesEqual(candidate.group_signature, group_signature);
});
if (it == m_display_seed_candidates.end())
{
m_display_seed_candidates.push_back(SeedCandidate{.signature = draw.signature,
.group_signature = group_signature,
.representative_draw = draw,
.occurrence_count = 1});
}
else
{
it->signature = draw.signature;
it->group_signature = group_signature;
it->representative_draw = draw;
it->occurrence_count++;
}
}
if (m_seed_signature.valid)
{
const auto it = std::find_if(m_display_seed_candidates.begin(), m_display_seed_candidates.end(),
[this](const SeedCandidate& candidate) {
return SignaturesEqual(candidate.group_signature,
m_seed_group_signature);
});
if (it == m_display_seed_candidates.end())
{
ClearSeedSelectionLocked();
}
else
{
m_selected_seed_index = static_cast<int>(std::distance(m_display_seed_candidates.begin(), it));
m_seed_signature = it->representative_draw.signature;
m_seed_group_signature = it->group_signature;
m_seed_draw = it->representative_draw;
}
}
if (m_display_matches.empty())
{
m_selected_match = 0;
m_display_highlighted_draw.reset();
m_display_highlighted_match_raw_draw_count = 0;
}
else if (m_selected_match >= static_cast<int>(m_display_matches.size()))
{
m_selected_match = 0;
}
if (!m_display_matches.empty() && !m_display_matches[m_selected_match].active_this_frame)
{
const auto it =
std::find_if(m_display_matches.begin(), m_display_matches.end(),
[](const CurrentMatchCandidate& candidate) { return candidate.active_this_frame; });
if (it != m_display_matches.end())
m_selected_match = static_cast<int>(std::distance(m_display_matches.begin(), it));
}
if (!m_display_matches.empty() && m_selected_match < static_cast<int>(m_display_matches.size()))
{
m_display_highlighted_draw = m_display_matches[m_selected_match].representative_draw;
m_display_highlighted_match_raw_draw_count = m_display_matches[m_selected_match].raw_draw_count;
}
else
{
m_display_highlighted_match_raw_draw_count = 0;
}
m_stable_submatch_occurrence_counters.clear();
m_current_stable_submatch = {};
}
bool ElementsGroupManager::HasActiveHuntLocked() const
{
return m_popup_open_count > 0 && m_hunt_enabled && m_seed_signature.valid &&
GroupMaskHasActiveGroups(m_group_mask.projection, m_group_mask.layer, m_group_mask.viewport,
m_group_mask.scissor, m_group_mask.render_state);
}
bool ElementsGroupManager::MatchesSelectedMatchFilterLocked(const DrawRecord& draw) const
{
if (m_selected_match_filters.empty())
return true;
const bool included =
MatchesSelectedMatchFilterSignatureLocked(MakeSelectedSubgroupSignature(draw));
return m_selected_match_filters_excluded ? !included : included;
}
bool ElementsGroupManager::MatchesSelectedMatchFilterSignatureLocked(
const SelectedSubgroupSignature& stable_signature) const
{
if (m_selected_match_filters.empty())
return true;
return std::any_of(m_selected_match_filters.begin(), m_selected_match_filters.end(),
[&stable_signature](const SelectedSubgroupSignature& filter) {
return SelectedSubgroupSignaturesEqual(filter, stable_signature);
});
}
std::vector<ElementsGroupManager::CurrentMatchCandidate>
ElementsGroupManager::ResolveSelectedMatchDisplayDrawsLocked() const
{
std::vector<CurrentMatchCandidate> result;
result.reserve(m_selected_match_filters.size());
for (const SelectedSubgroupSignature& filter : m_selected_match_filters)
{
const auto it = std::find_if(m_display_matches.begin(), m_display_matches.end(),
[&filter](const CurrentMatchCandidate& candidate) {
return SelectedSubgroupSignaturesEqual(candidate.subgroup, filter);
});
if (it != m_display_matches.end())
result.push_back(*it);
else
result.push_back(CurrentMatchCandidate{.subgroup = filter,
.representative_draw = DrawRecord{.draw_index = -1},
.raw_draw_count = 0});
}
return result;
}
ElementsGroupManager::StableSubMatchSignature
ElementsGroupManager::GetStableSubMatchSignatureLocked(const DrawRecord& draw) const
{
if (m_current_stable_submatch.valid && m_current_stable_submatch.draw_sequence == draw.draw_sequence)
return m_current_stable_submatch.signature;
const StableSubMatchSignature base_signature = MakeStableSubMatchSignature(draw, -1);
const u64 base_key = static_cast<u64>(ComputeStableSubMatchBaseKey(base_signature));
const int occurrence_slot = m_stable_submatch_occurrence_counters[base_key]++;
m_current_stable_submatch.valid = true;
m_current_stable_submatch.draw_sequence = draw.draw_sequence;
m_current_stable_submatch.signature = MakeStableSubMatchSignature(draw, occurrence_slot);
return m_current_stable_submatch.signature;
}
void ElementsGroupManager::AdvanceOverrideDrawCounters(const DrawRecord& draw)
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.element_start < 0 || entry.element_end < 0)
continue;
if (!DoesEntryMatchForRange(entry, draw))
continue;
const CounterKey key = GetCounterKey(entry);
m_current_draw_indices[key] = m_draw_counters[key];
m_draw_counters[key]++;
}
}
bool ElementsGroupManager::DoesTextureFilterPass(const DrawRecord& draw,
const ElementGroupOverride& entry) const
{
if (entry.texture_hashes.empty())
return true;
bool any_match = false;
for (u64 current_hash : draw.textures)
{
if (current_hash == 0)
continue;
if (std::find(entry.texture_hashes.begin(), entry.texture_hashes.end(), current_hash) !=
entry.texture_hashes.end())
{
any_match = true;
break;
}
}
return entry.texture_hashes_excluded ? !any_match : any_match;
}
ElementsGroupManager::CounterKey ElementsGroupManager::GetCounterKey(
const ElementGroupOverride& entry) const
{
size_t seed = 0;
HashCombineValue(seed, static_cast<int>(entry.match_kind));
if (entry.match_kind == MatchKind::ProfileLayer)
{
HashCombineValue(seed, ComputeProfileElementKey(entry.profile_id, entry.profile_layers,
entry.texture_hashes,
entry.texture_hashes_excluded));
}
else
{
HashCombineValue(seed, ComputeRuntimeElementKey(entry.runtime_element, entry.texture_hashes,
entry.texture_hashes_excluded));
}
for (const SelectedSubgroupSignature& filter : entry.selected_match_filter)
{
HashCombineValue(seed, ComputeSelectedSubgroupKey(filter));
}
HashCombineValue(seed, entry.selected_match_filter_excluded);
return CounterKey{static_cast<u64>(seed)};
}
std::pair<int, int> ElementsGroupManager::ResolveElementRange(const ElementGroupOverride& entry,
const DrawRecord& draw) const
{
if (entry.element_start < 0 || entry.element_end < 0)
return {-1, -1};
const CounterKey key = GetCounterKey(entry);
const auto total_it = m_draw_totals_prev.find(key);
const int current_total = total_it != m_draw_totals_prev.end() ? total_it->second :
entry.element_reference_total;
const int reference_total = entry.element_reference_total;
if (current_total <= 0 || reference_total <= 0 || current_total == reference_total)
return {entry.element_start, entry.element_end};
const double scale = static_cast<double>(current_total) / static_cast<double>(reference_total);
const int range_start = std::clamp(static_cast<int>(std::lround(entry.element_start * scale)),
0, current_total - 1);
const int range_end = std::clamp(static_cast<int>(std::lround(entry.element_end * scale)),
0, current_total - 1);
return {std::min(range_start, range_end), std::max(range_start, range_end)};
}
bool ElementsGroupManager::DoesEntryMatchForRange(const ElementGroupOverride& entry,
const DrawRecord& draw) const
{
if (!entry.enabled)
return false;
if (entry.match_kind == MatchKind::ProfileLayer)
{
if (entry.profile_id == MetroidElementProfile::None || entry.profile_layers.empty() ||
draw.profile_id != entry.profile_id)
{
return false;
}
if (std::find(entry.profile_layers.begin(), entry.profile_layers.end(), draw.profile_layer) ==
entry.profile_layers.end())
{
return false;
}
}
else
{
if (!entry.runtime_element.valid)
return false;
if (!RuntimeElementMatcher::Matches(entry.runtime_element, draw.signature))
return false;
}
if (!DoesSelectedMatchFilterPass(entry, draw))
return false;
if (!DoesTextureFilterPass(draw, entry))
return false;
return true;
}
bool ElementsGroupManager::DoesSelectedMatchFilterPass(const ElementGroupOverride& entry,
const DrawRecord& draw) const
{
if (entry.selected_match_filter.empty())
return true;
const SelectedSubgroupSignature stable_signature = MakeSelectedSubgroupSignature(draw);
const bool included =
std::any_of(entry.selected_match_filter.begin(), entry.selected_match_filter.end(),
[&stable_signature](const SelectedSubgroupSignature& filter) {
return SelectedSubgroupSignaturesEqual(filter, stable_signature);
});
return entry.selected_match_filter_excluded ? !included : included;
}
bool ElementsGroupManager::DoesEntryMatch(const ElementGroupOverride& entry, const DrawRecord& draw,
bool include_condition) const
{
if (!DoesEntryMatchForRange(entry, draw))
return false;
if (entry.element_start >= 0 && entry.element_end >= 0)
{
const auto [range_start, range_end] = ResolveElementRange(entry, draw);
const CounterKey key = GetCounterKey(entry);
const auto current_it = m_current_draw_indices.find(key);
const int current_draw_index = current_it != m_current_draw_indices.end() ? current_it->second : -1;
if (current_draw_index < range_start || current_draw_index > range_end)
return false;
}
if (include_condition && !entry.condition_flag.empty())
{
const bool active = ShaderHunter::GetInstance().IsFlagActive(entry.condition_flag);
if (entry.condition_inverted ? active : !active)
return false;
}
return true;
}
void ElementsGroupManager::RegisterFlagsForDraw(const DrawRecord& draw)
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.flag_group.empty())
continue;
if (!DoesEntryMatch(entry, draw, true))
continue;
if (ShaderHunter::GetInstance().IsDebugLogging())
{
INFO_LOG_FMT(VIDEO, "ElementsGroup match(flag): '{}' draw#{} flag={}", entry.name,
draw.draw_index + 1, entry.flag_group);
}
ShaderHunter::GetInstance().RegisterExternalFlag(entry.flag_group);
}
}
bool ElementsGroupManager::ShouldSkipByOverride(const DrawRecord& draw) const
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.handling != HandlingType::Skip)
continue;
if (DoesEntryMatch(entry, draw, true))
{
if (ShaderHunter::GetInstance().IsDebugLogging())
{
INFO_LOG_FMT(VIDEO, "ElementsGroup match(skip): '{}' draw#{}", entry.name,
draw.draw_index + 1);
}
return true;
}
}
return false;
}
ElementsGroupManager::HandlingType ElementsGroupManager::GetOverrideHandling(
const DrawRecord& draw) const
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.handling == HandlingType::Skip || entry.handling == HandlingType::Flag)
continue;
if (DoesEntryMatch(entry, draw, true))
{
if (ShaderHunter::GetInstance().IsDebugLogging())
{
INFO_LOG_FMT(VIDEO, "ElementsGroup match(handling): '{}' draw#{} handling={}", entry.name,
draw.draw_index + 1, static_cast<int>(entry.handling));
}
return entry.handling;
}
}
return HandlingType::Skip;
}
int ElementsGroupManager::GetOverrideLayer(const DrawRecord& draw) const
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.layer < 0)
continue;
if (DoesEntryMatch(entry, draw, true))
return entry.layer;
}
return -1;
}
float ElementsGroupManager::GetOverrideElementDepth(const DrawRecord& draw) const
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.element_depth < 0.0f)
continue;
if (DoesEntryMatch(entry, draw, true))
return entry.element_depth;
}
return -1.0f;
}
float ElementsGroupManager::GetOverrideUnitsPerMeter(const DrawRecord& draw) const
{
std::lock_guard lock(m_mutex);
GetStableSubMatchSignatureLocked(draw);
for (const auto& entry : m_overrides)
{
if (entry.units_per_meter <= 0.0f)
continue;
if (DoesEntryMatch(entry, draw, true))
return entry.units_per_meter;
}
return -1.0f;
}