// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/ShaderHunter.h" #include #include #include #include #include #include #include "Common/CommonPaths.h" #include "Common/FileUtil.h" #include "Common/Hash.h" #include "Common/Logging/Log.h" #include "Core/ConfigLoaders/GameConfigLoader.h" #include "VideoCommon/GeometryShaderGen.h" #include "VideoCommon/PixelShaderGen.h" #include "VideoCommon/ShaderGenCommon.h" #include "VideoCommon/VertexShaderGen.h" #include "VideoCommon/VideoConfig.h" namespace { constexpr u64 FAMILY_HASH_OFFSET = 1469598103934665603ULL; constexpr u64 FAMILY_HASH_PRIME = 1099511628211ULL; void MixFamilyHash(u64& hash, u64 value) { hash ^= value; hash *= FAMILY_HASH_PRIME; } u64 ComputePixelFamilySignature(const pixel_shader_uid_data& uid) { u64 signature = FAMILY_HASH_OFFSET; // Keep TEV/shader-logic identity, ignore fog and other scene-variant state. MixFamilyHash(signature, uid.nIndirectStagesUsed); MixFamilyHash(signature, uid.genMode_numtexgens); MixFamilyHash(signature, uid.genMode_numtevstages); MixFamilyHash(signature, uid.genMode_numindstages); MixFamilyHash(signature, uid.numColorChans); MixFamilyHash(signature, static_cast(uid.Pretest)); MixFamilyHash(signature, static_cast(uid.alpha_test_comp0)); MixFamilyHash(signature, static_cast(uid.alpha_test_comp1)); MixFamilyHash(signature, static_cast(uid.alpha_test_logic)); MixFamilyHash(signature, static_cast(uid.ztex_op)); MixFamilyHash(signature, uid.per_pixel_depth); MixFamilyHash(signature, static_cast(uid.ztest)); MixFamilyHash(signature, uid.tevindref_bi0); MixFamilyHash(signature, uid.tevindref_bc0); MixFamilyHash(signature, uid.tevindref_bi1); MixFamilyHash(signature, uid.tevindref_bc1); MixFamilyHash(signature, uid.tevindref_bi2); MixFamilyHash(signature, uid.tevindref_bc2); MixFamilyHash(signature, uid.tevindref_bi3); MixFamilyHash(signature, uid.tevindref_bc3); const int stage_count = std::clamp(static_cast(uid.genMode_numtevstages) + 1, 1, 16); for (int i = 0; i < stage_count; ++i) { const auto& stage = uid.stagehash[i]; MixFamilyHash(signature, stage.cc); MixFamilyHash(signature, stage.ac); MixFamilyHash(signature, stage.tevorders_texmap); MixFamilyHash(signature, stage.tevorders_texcoord); MixFamilyHash(signature, stage.tevorders_enable); MixFamilyHash(signature, static_cast(stage.tevorders_colorchan)); MixFamilyHash(signature, stage.tevind); MixFamilyHash(signature, static_cast(stage.ras_swap_r)); MixFamilyHash(signature, static_cast(stage.ras_swap_g)); MixFamilyHash(signature, static_cast(stage.ras_swap_b)); MixFamilyHash(signature, static_cast(stage.ras_swap_a)); MixFamilyHash(signature, static_cast(stage.tex_swap_r)); MixFamilyHash(signature, static_cast(stage.tex_swap_g)); MixFamilyHash(signature, static_cast(stage.tex_swap_b)); MixFamilyHash(signature, static_cast(stage.tex_swap_a)); MixFamilyHash(signature, static_cast(stage.tevksel_kc)); MixFamilyHash(signature, static_cast(stage.tevksel_ka)); } return signature; } u64 ComputeVertexFamilySignature(const vertex_shader_uid_data& uid) { u64 signature = FAMILY_HASH_OFFSET; // Game-derived vertex configuration only. Deliberately excluded so signatures survive shader // updates and match across devices: code_version (bumped on every generator change), vs_expand // (backend capability, differs between PC and Quest), and lighting.light_mask (scene-variant as // games toggle lights at runtime). MixFamilyHash(signature, uid.components); MixFamilyHash(signature, uid.numTexGens); MixFamilyHash(signature, uid.numColorChans); MixFamilyHash(signature, uid.dualTexTrans_enabled); MixFamilyHash(signature, uid.position_has_3_elems); MixFamilyHash(signature, uid.texcoord_elem_count); MixFamilyHash(signature, uid.texMtxInfo_n_projection); const u32 texgen_count = std::min(uid.numTexGens, 8); for (u32 i = 0; i < texgen_count; ++i) { const auto& texinfo = uid.texMtxInfo[i]; MixFamilyHash(signature, static_cast(texinfo.inputform)); MixFamilyHash(signature, static_cast(texinfo.texgentype)); MixFamilyHash(signature, static_cast(texinfo.sourcerow)); MixFamilyHash(signature, texinfo.embosssourceshift); MixFamilyHash(signature, texinfo.embosslightshift); const auto& postinfo = uid.postMtxInfo[i]; MixFamilyHash(signature, postinfo.index); MixFamilyHash(signature, postinfo.normalize); } MixFamilyHash(signature, uid.lighting.matsource); MixFamilyHash(signature, uid.lighting.enablelighting); MixFamilyHash(signature, uid.lighting.ambsource); MixFamilyHash(signature, uid.lighting.diffusefunc); MixFamilyHash(signature, uid.lighting.attnfunc); return signature; } u64 ComputeGeometryFamilySignature(const geometry_shader_uid_data& uid) { u64 signature = FAMILY_HASH_OFFSET; // Everything game-derived in the GS UID; code_version is excluded (bumped on every generator // change). MixFamilyHash(signature, uid.numTexGens); MixFamilyHash(signature, uid.primitive_type); return signature; } u64 ComputeShaderFamilySignature(ShaderHunter::ShaderType type, const u8* uid_data, size_t uid_size) { if (!uid_data || uid_size == 0) return 0; if (type == ShaderHunter::ShaderType::Pixel && uid_size >= sizeof(pixel_shader_uid_data)) { const auto* ps_uid = reinterpret_cast(uid_data); return ComputePixelFamilySignature(*ps_uid); } if (type == ShaderHunter::ShaderType::Vertex && uid_size >= sizeof(vertex_shader_uid_data)) { const auto* vs_uid = reinterpret_cast(uid_data); return ComputeVertexFamilySignature(*vs_uid); } if (type == ShaderHunter::ShaderType::Geometry && uid_size >= sizeof(geometry_shader_uid_data)) { const auto* gs_uid = reinterpret_cast(uid_data); return ComputeGeometryFamilySignature(*gs_uid); } return static_cast(Common::ComputeCRC32(uid_data, static_cast(uid_size))); } std::vector GetSortedTextureHashes( const std::unordered_map& texture_usage) { std::vector hashes; hashes.reserve(texture_usage.size()); for (const auto& [texture_hash, _] : texture_usage) hashes.push_back(texture_hash); std::sort(hashes.begin(), hashes.end()); hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end()); return hashes; } // Collapse an override's flat anchor fields into the shared transport struct. ShaderHunter::CameraAnchorParams MakeCameraAnchorParams(const ShaderHunter::ShaderOverride& ovr) { return ShaderHunter::CameraAnchorParams{ .offset = {ovr.anchor_right, ovr.anchor_up, ovr.anchor_forward}, .hide = ovr.anchor_hide, .rotation = ovr.anchor_rotation, .yaw_offset_deg = ovr.anchor_yaw_deg, .units_per_meter = ovr.anchor_units_per_meter}; } ShaderHunter::ControllerAnchorParams MakeControllerAnchorParams( const ShaderHunter::ShaderOverride& ovr) { return ShaderHunter::ControllerAnchorParams{ .hand = ovr.anchor_hand == 0 ? 0 : 1, .offset = {ovr.anchor_right, ovr.anchor_up, ovr.anchor_forward}, .rotation = ovr.anchor_rotation != ShaderHunter::AnchorRotationMode::Off, .yaw_deg = ovr.anchor_yaw_deg, .pitch_deg = ovr.anchor_pitch_deg, .roll_deg = ovr.anchor_roll_deg}; } } // namespace ShaderHunter& ShaderHunter::GetInstance() { static ShaderHunter instance; return instance; } u64 ShaderHunter::RegisterShader(ShaderType type, u64 hash, const u8* uid_data, size_t uid_size) { const u64 family_signature = ComputeShaderFamilySignature(type, uid_data, uid_size); std::lock_guard lock(m_mutex); const int t = static_cast(type); if (hash != 0 && family_signature != 0) m_shader_family_signatures[t][hash] = family_signature; if (m_enabled) m_collecting[t].insert(hash); // Cache UID bytes for later shader source regeneration (dump) if (m_enabled && m_uid_cache[t].find(hash) == m_uid_cache[t].end() && uid_data && uid_size > 0) m_uid_cache[t][hash] = std::vector(uid_data, uid_data + uid_size); return family_signature; } std::optional ShaderHunter::GetShaderFamilySignature(ShaderType type, u64 hash) const { if (hash == 0) return std::nullopt; std::lock_guard lock(m_mutex); const auto& per_type = m_shader_family_signatures[static_cast(type)]; const auto it = per_type.find(hash); if (it == per_type.end() || it->second == 0) return std::nullopt; return it->second; } void ShaderHunter::SetCurrentDrawShaderFamilies(u64 vs_family, u64 ps_family, u64 gs_family) { m_current_vs_family = vs_family; m_current_ps_family = ps_family; m_current_gs_family = gs_family; } void ShaderHunter::SetCurrentDrawSignature(const RuntimeElementSignature& signature) { m_current_draw_signature = signature; } void ShaderHunter::RegisterDrawCombination(u64 vs_hash, u64 ps_hash, u64 gs_hash) { if (!m_enabled) return; std::lock_guard lock(m_mutex); const auto record_textures_for_shader = [this](ShaderType type, u64 shader_hash) { auto& shader_map = m_shader_texture_usage_collecting[static_cast(type)]; auto& textures_for_hash = shader_map[shader_hash]; for (int i = 0; i < 8; i++) { const u64 texture_hash = m_current_draw_textures[i]; if (texture_hash == 0) continue; auto& name = textures_for_hash[texture_hash]; if (name.empty()) name = m_current_draw_texture_names[i]; } }; record_textures_for_shader(ShaderType::Vertex, vs_hash); record_textures_for_shader(ShaderType::Pixel, ps_hash); record_textures_for_shader(ShaderType::Geometry, gs_hash); } void ShaderHunter::OnFrameEnd() { // Always update flags, even when hunting is disabled (flags serve persistent overrides). // Log activations/deactivations. for (const auto& flag : m_flags_seen_this_frame) { if (m_flag_age.find(flag) == m_flag_age.end()) INFO_LOG_FMT(VIDEO, "ShaderHunter: Flag '{}' activated", flag); } for (const auto& [flag, _] : m_flag_age) { if (m_flags_seen_this_frame.count(flag) == 0) INFO_LOG_FMT(VIDEO, "ShaderHunter: Flag '{}' deactivated", flag); } // Swap: only flags seen this frame are active next frame. m_flag_age.clear(); for (const auto& flag : m_flags_seen_this_frame) m_flag_age[flag] = 0; m_flags_seen_this_frame.clear(); // Clear debug logging per-frame dedup set m_debug_logged_combos.clear(); if (!m_enabled) return; std::lock_guard lock(m_mutex); for (int i = 0; i < TYPE_COUNT; i++) { m_display[i] = std::move(m_collecting[i]); m_collecting[i].clear(); } m_shader_texture_usage_display = std::move(m_shader_texture_usage_collecting); for (auto& per_type : m_shader_texture_usage_collecting) per_type.clear(); m_texture_usage_display = std::move(m_texture_usage_collecting); m_texture_usage_collecting.clear(); } bool ShaderHunter::ShouldSkipDraw(u64 vs_hash, u64 ps_hash, u64 gs_hash) { if (!m_enabled) { m_should_highlight_selected_draw = false; return false; } std::lock_guard lock(m_mutex); const int t = static_cast(m_active_type); if (m_selected_pos[t] < 0) { m_should_highlight_selected_draw = false; m_selected_draw_signature = {}; return false; } const u64 sel = m_selected_hash[t]; bool matches = false; switch (m_active_type) { case ShaderType::Pixel: matches = (ps_hash == sel); break; case ShaderType::Vertex: matches = (vs_hash == sel); break; case ShaderType::Geometry: matches = (gs_hash == sel); break; default: m_should_highlight_selected_draw = false; return false; } if (!matches) { m_should_highlight_selected_draw = false; m_selected_draw_signature = {}; return false; } // Record unique textures used by this selected shader in the current frame. for (int i = 0; i < 8; i++) { const u64 texture_hash = m_current_draw_textures[i]; if (texture_hash == 0) continue; auto& name = m_texture_usage_collecting[texture_hash]; if (name.empty()) name = m_current_draw_texture_names[i]; } bool selected_draw_matches_hunting = false; // Texture-filter mode: target the union of the currently browsed texture hash // and all manually added hashes. if (m_selected_texture_hash != 0 || m_texture_skip_mode_active) { bool has_any_texture_target = false; if (m_selected_texture_hash != 0) { has_any_texture_target = true; for (u64 texture_hash : m_current_draw_textures) { if (texture_hash == m_selected_texture_hash) { selected_draw_matches_hunting = true; break; } } } if (!selected_draw_matches_hunting && !m_texture_skip_filters.empty()) { has_any_texture_target = true; for (u64 texture_hash : m_current_draw_textures) { if (texture_hash != 0 && m_texture_skip_filters.count(texture_hash) > 0) { selected_draw_matches_hunting = true; break; } } } if (!has_any_texture_target) { m_should_highlight_selected_draw = false; return false; } } else { // Normal mode: target all draws with this selected hash. selected_draw_matches_hunting = true; } m_should_highlight_selected_draw = selected_draw_matches_hunting && m_hunting_option == HuntingOption::Pink; if (!selected_draw_matches_hunting) { m_selected_draw_signature = {}; return false; } m_selected_draw_signature = m_current_draw_signature; return m_hunting_option == HuntingOption::Skip; } void ShaderHunter::SetEnabled(bool enabled) { std::lock_guard lock(m_mutex); m_enabled = enabled; if (!enabled) { m_should_highlight_selected_draw = false; for (int i = 0; i < TYPE_COUNT; i++) { m_collecting[i].clear(); m_display[i].clear(); m_selected_hash[i] = ~0ULL; m_selected_pos[i] = -1; } m_texture_skip_filters.clear(); m_texture_skip_mode_active = false; m_texture_tool_active.store(false, std::memory_order_relaxed); m_selected_texture_hash = 0; m_current_vs_family = 0; m_current_ps_family = 0; m_current_gs_family = 0; m_current_draw_signature = {}; m_selected_draw_signature = {}; m_texture_usage_collecting.clear(); m_texture_usage_display.clear(); for (auto& per_type : m_shader_texture_usage_collecting) per_type.clear(); for (auto& per_type : m_shader_texture_usage_display) per_type.clear(); } } bool ShaderHunter::IsEnabled() const { return m_enabled; } void ShaderHunter::SetHuntingOption(HuntingOption option) { std::lock_guard lock(m_mutex); m_hunting_option = option; } ShaderHunter::HuntingOption ShaderHunter::GetHuntingOption() const { std::lock_guard lock(m_mutex); return m_hunting_option; } bool ShaderHunter::ShouldHighlightSelectedDraw() const { std::lock_guard lock(m_mutex); return m_should_highlight_selected_draw; } void ShaderHunter::SetActiveType(ShaderType type) { std::lock_guard lock(m_mutex); if (m_active_type != type) { m_texture_skip_filters.clear(); m_texture_skip_mode_active = false; m_selected_texture_hash = 0; m_texture_usage_collecting.clear(); m_texture_usage_display.clear(); m_selected_draw_signature = {}; } m_active_type = type; } ShaderHunter::ShaderType ShaderHunter::GetActiveType() const { return m_active_type; } void ShaderHunter::NextShader() { std::lock_guard lock(m_mutex); const int t = static_cast(m_active_type); auto& visited = m_display[t]; if (visited.empty()) return; auto it = visited.find(m_selected_hash[t]); if (it != visited.end()) { ++it; if (it != visited.end()) { m_selected_pos[t]++; } else { it = visited.begin(); m_selected_pos[t] = 0; } } else { it = visited.begin(); m_selected_pos[t] = 0; } const u64 previous = m_selected_hash[t]; m_selected_hash[t] = *it; if (previous != m_selected_hash[t]) { m_texture_skip_filters.clear(); m_texture_skip_mode_active = false; m_selected_texture_hash = 0; m_texture_usage_collecting.clear(); m_texture_usage_display.clear(); m_selected_draw_signature = {}; } } void ShaderHunter::PrevShader() { std::lock_guard lock(m_mutex); const int t = static_cast(m_active_type); auto& visited = m_display[t]; if (visited.empty()) return; auto it = visited.find(m_selected_hash[t]); if (it != visited.end()) { if (it != visited.begin()) { --it; m_selected_pos[t]--; } else { it = std::prev(visited.end()); m_selected_pos[t] = static_cast(visited.size()) - 1; } } else { it = std::prev(visited.end()); m_selected_pos[t] = static_cast(visited.size()) - 1; } const u64 previous = m_selected_hash[t]; m_selected_hash[t] = *it; if (previous != m_selected_hash[t]) { m_texture_skip_filters.clear(); m_texture_skip_mode_active = false; m_selected_texture_hash = 0; m_texture_usage_collecting.clear(); m_texture_usage_display.clear(); m_selected_draw_signature = {}; } } bool ShaderHunter::SelectShader(ShaderType type, u64 hash) { std::lock_guard lock(m_mutex); const int t = static_cast(type); auto& visited = m_display[t]; if (visited.empty()) return false; const auto it = visited.find(hash); if (it == visited.end()) return false; const bool type_changed = (m_active_type != type); const bool hash_changed = (m_selected_hash[t] != hash); if (type_changed || hash_changed) { m_texture_skip_filters.clear(); m_texture_skip_mode_active = false; m_selected_texture_hash = 0; m_texture_usage_collecting.clear(); m_texture_usage_display.clear(); m_selected_draw_signature = {}; } m_active_type = type; m_selected_hash[t] = hash; m_selected_pos[t] = static_cast(std::distance(visited.begin(), it)); return true; } u64 ShaderHunter::GetSelectedHash() const { std::lock_guard lock(m_mutex); return m_selected_hash[static_cast(m_active_type)]; } int ShaderHunter::GetSelectedPosition() const { std::lock_guard lock(m_mutex); return m_selected_pos[static_cast(m_active_type)]; } int ShaderHunter::GetTotalCount() const { std::lock_guard lock(m_mutex); return static_cast(m_display[static_cast(m_active_type)].size()); } void ShaderHunter::SetCurrentDrawTextures(const std::array& hashes, const std::array& names) { m_current_draw_textures = hashes; m_current_draw_texture_names = names; } // ============================================================================ // Static INI helpers — direct file I/O to avoid IniFile's key-value parsing // corrupting our raw-line sections. // ============================================================================ // Helper: read file contents, stripping [ShaderOverride_Enable] and [ShaderOverride] sections. static std::string ReadFileWithoutShaderSections(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 == "[ShaderOverride_Enable]" || trimmed == "[ShaderOverride]") { skipping = true; continue; } if (skipping && !trimmed.empty() && trimmed[0] == '[') skipping = false; if (!skipping) out << line << "\n"; } return out.str(); } // Helper: parse a key=value line with optional whitespace around '='. static 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; } // Helper: parse one or more hex texture hashes from a comma/space/semicolon separated list. // Invalid tokens are ignored. static std::vector ParseTextureHashList(const std::string& value) { std::vector 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(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(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; } // Helper: evaluate texture hash filter against currently bound textures. // Include mode: pass when any listed hash matches. // Exclude mode: pass when none of the listed hashes match. static bool DoesTextureFilterPass(const std::array& current_textures, const std::vector& filter_hashes, bool exclude_mode) { if (filter_hashes.empty()) return true; bool any_match = false; for (u64 current_hash : current_textures) { if (current_hash == 0) continue; if (std::find(filter_hashes.begin(), filter_hashes.end(), current_hash) != filter_hashes.end()) { any_match = true; break; } } return exclude_mode ? !any_match : any_match; } static std::string GetVRGameSettingsPath(const std::string& game_id) { return File::GetUserPath(D_GAMESETTINGSVR_IDX) + game_id + ".ini"; } static std::string GetSysVRGameSettingsPath(const std::string& filename) { return File::GetSysDirectory() + GAMESETTINGSVR_DIR DIR_SEP + filename; } struct ParsedShaderOverrideFile { std::vector entries; bool has_enable_section = false; std::set enabled_names; }; ParsedShaderOverrideFile LoadShaderOverridesFromINIFile(const std::string& path) { using ShaderOverride = ShaderHunter::ShaderOverride; using ShaderType = ShaderHunter::ShaderType; using HandlingType = ShaderHunter::HandlingType; using MatchMode = ShaderHunter::MatchMode; using AnchorRotationMode = ShaderHunter::AnchorRotationMode; ParsedShaderOverrideFile parsed; std::ifstream file(path); if (!file.is_open()) return parsed; // First pass: read [ShaderOverride_Enable] to get enabled names { bool in_section = false; std::string line; while (std::getline(file, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); if (line == "[ShaderOverride_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)); } } // Second pass: read [ShaderOverride] for override data file.clear(); file.seekg(0); bool in_section = false; ShaderOverride current{}; bool has_entry = false; auto commit_entry = [&]() { if (!has_entry || current.hash == 0 || current.match_mode == MatchMode::RuntimeElement) return; std::sort(current.texture_hashes.begin(), current.texture_hashes.end()); current.texture_hashes.erase( std::unique(current.texture_hashes.begin(), current.texture_hashes.end()), current.texture_hashes.end()); // Backward compatibility: old INIs may not have [ShaderOverride_Enable]. // In that case, treat all entries as enabled. current.enabled = parsed.has_enable_section ? (parsed.enabled_names.count(current.name) > 0) : true; parsed.entries.push_back(current); }; std::string line; while (std::getline(file, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); if (line == "[ShaderOverride]") { 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.hash = 0; current.type = ShaderType::Pixel; current.enabled = false; current.user_defined = true; current.condition_flag.clear(); current.condition_inverted = false; current.match_mode = MatchMode::ExactHash; // Entries saved before the family_version key existed used the v1 scheme (raw-UID CRC32 // for VS/GS family signatures). current.family_version = 1; has_entry = true; } else if (has_entry) { // Backward compatibility: accept legacy '+' prefixes from older files. std::string data_line = line; if (!data_line.empty() && data_line[0] == '+') data_line = data_line.substr(1); std::string key, value; if (!ParseKeyValue(data_line, key, value)) continue; if (key == "Hash") current.hash = std::strtoull(value.c_str(), nullptr, 16); else if (key == "Type") { if (value == "PS") current.type = ShaderType::Pixel; else if (value == "VS") current.type = ShaderType::Vertex; else if (value == "GS") current.type = ShaderType::Geometry; } else if (key == "handling") { if (value == "screen") current.handling = HandlingType::Screen; else if (value == "fullscreen") current.handling = HandlingType::Fullscreen; else if (value == "fullscreen_mono" || value == "fullscreenmono") current.handling = HandlingType::Fullscreen; else if (value == "headlocked") current.handling = HandlingType::HeadLocked; else if (value == "flag") current.handling = HandlingType::Flag; else if (value == "units_per_meter" || value == "unitspermeter" || value == "upm") current.handling = HandlingType::UnitsPerMeter; else if (value == "passthrough") current.handling = HandlingType::Passthrough; else if (value == "camera_anchor" || value == "cameraanchor") current.handling = HandlingType::CameraAnchor; else if (value == "controller_anchor" || value == "controlleranchor") current.handling = HandlingType::ControllerAnchor; else current.handling = HandlingType::Skip; } // "layer" (manual depth layer) was removed along with Auto Layer Spread / Layer Offset — // silently ignored so older INIs still load. 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 == "passthrough_opacity") { current.passthrough_opacity = std::clamp(std::stof(value), 0.0f, 1.0f); } else if (key == "anchor_right") { current.anchor_right = std::stof(value); } else if (key == "anchor_up") { current.anchor_up = std::stof(value); } else if (key == "anchor_forward") { current.anchor_forward = std::stof(value); } else if (key == "anchor_hide") { current.anchor_hide = (value == "1" || value == "true"); } else if (key == "anchor_rotation") { current.anchor_rotation = value == "full" ? AnchorRotationMode::Full : value == "yaw" ? AnchorRotationMode::YawOnly : AnchorRotationMode::Off; } else if (key == "anchor_yaw") { current.anchor_yaw_deg = std::stof(value); } else if (key == "anchor_upm" || key == "anchor_units_per_meter") { current.anchor_units_per_meter = std::stof(value); } else if (key == "anchor_hand") { current.anchor_hand = (value == "left" || value == "0") ? 0 : 1; } else if (key == "anchor_pitch") { current.anchor_pitch_deg = std::stof(value); } else if (key == "anchor_roll") { current.anchor_roll_deg = std::stof(value); } else if (key == "flag") { current.flag_group = value; } else if (key == "condition") { std::string cond = value; // Legacy compatibility: support inline negation ("!flag", "not:flag"). if (!cond.empty() && cond[0] == '!') { current.condition_inverted = true; cond.erase(cond.begin()); } else if (cond.rfind("not:", 0) == 0) { current.condition_inverted = true; cond = cond.substr(4); } current.condition_flag = cond; } else if (key == "condition_mode") { std::string mode = value; std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (mode == "deactivate" || mode == "inactive" || mode == "not" || mode == "1" || mode == "true") { current.condition_inverted = true; } else if (mode == "activate" || mode == "active" || mode == "0" || mode == "false") { current.condition_inverted = false; } } 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") { std::string mode = value; std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (mode == "exclude" || mode == "excluded" || mode == "not") current.texture_hashes_excluded = true; else if (mode == "include" || mode == "included") current.texture_hashes_excluded = false; } else if (key == "match_mode") { std::string mode = value; std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (mode == "runtime_element" || mode == "runtimeelement") current.match_mode = MatchMode::RuntimeElement; else if (mode == "shader_family" || mode == "shaderfamily" || mode == "family") current.match_mode = MatchMode::ShaderFamily; else current.match_mode = MatchMode::ExactHash; } else if (key == "hash_family") { std::string mode = value; std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); current.hash_family_match = (mode == "1" || mode == "true" || mode == "yes" || mode == "on" || mode == "family"); if (current.hash_family_match) current.match_mode = MatchMode::ShaderFamily; } else if (key == "family_signature") { current.family_signature = std::strtoull(value.c_str(), nullptr, 16); } else if (key == "family_version") { current.family_version = static_cast(std::strtoul(value.c_str(), nullptr, 10)); if (current.family_version == 0) current.family_version = 1; } } } commit_entry(); return parsed; } static void MergeParsedShaderOverrideFile(std::vector* result, std::map* index_by_name, ParsedShaderOverrideFile 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 ShaderHunter::LoadOverridesFromINI(const std::string& game_id, std::optional revision) { if (game_id.empty()) return {}; std::vector result; std::map index_by_name; for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision)) MergeParsedShaderOverrideFile(&result, &index_by_name, LoadShaderOverridesFromINIFile(GetSysVRGameSettingsPath(filename))); for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision)) MergeParsedShaderOverrideFile( &result, &index_by_name, LoadShaderOverridesFromINIFile(File::GetUserPath(D_GAMESETTINGSVR_IDX) + filename)); return result; } void ShaderHunter::SaveOverridesToINI(const std::string& game_id, const std::vector& overrides) { if (game_id.empty()) return; const std::string path = GetVRGameSettingsPath(game_id); File::CreateFullPath(path); std::string base = ReadFileWithoutShaderSections(path); // Strip trailing whitespace/newlines, then add one newline separator while (!base.empty() && (base.back() == '\n' || base.back() == '\r' || base.back() == ' ')) base.pop_back(); if (!base.empty()) base += "\n"; std::ostringstream out; out << base; // [ShaderOverride_Enable] — lists enabled override names out << "[ShaderOverride_Enable]\n"; for (const auto& ovr : overrides) { if (ovr.enabled) out << "$" << ovr.name << "\n"; } // [ShaderOverride] — full override data (all overrides, enabled or not) // Data lines are written as plain key=value in GameSettingsVR. out << "[ShaderOverride]\n"; for (const auto& ovr : overrides) { const char* type_str = ovr.type == ShaderType::Vertex ? "VS" : ovr.type == ShaderType::Geometry ? "GS" : "PS"; const char* handling_str = ovr.handling == HandlingType::Screen ? "screen" : ovr.handling == HandlingType::Fullscreen || ovr.handling == HandlingType::FullscreenMono ? "fullscreen" : ovr.handling == HandlingType::HeadLocked ? "headlocked" : ovr.handling == HandlingType::Flag ? "flag" : ovr.handling == HandlingType::UnitsPerMeter ? "units_per_meter" : ovr.handling == HandlingType::Passthrough ? "passthrough" : ovr.handling == HandlingType::CameraAnchor ? "camera_anchor" : ovr.handling == HandlingType::ControllerAnchor ? "controller_anchor" : "skip"; out << "$" << ovr.name << "\n"; out << "Hash=" << fmt::format("{:016x}", ovr.hash) << "\n"; out << "Type=" << type_str << "\n"; out << "match_mode=" << (ovr.match_mode == MatchMode::ShaderFamily ? "shader_family" : "exact_hash") << "\n"; out << "handling=" << handling_str << "\n"; if (ovr.element_depth >= 0.0f) out << "element_depth=" << ovr.element_depth << "\n"; if (ovr.handling == HandlingType::UnitsPerMeter && ovr.units_per_meter > 0.0f) out << "units_per_meter=" << ovr.units_per_meter << "\n"; if (ovr.handling == HandlingType::Passthrough) out << "passthrough_opacity=" << ovr.passthrough_opacity << "\n"; if (ovr.handling == HandlingType::CameraAnchor) { if (ovr.anchor_right != 0.0f) out << "anchor_right=" << ovr.anchor_right << "\n"; if (ovr.anchor_up != 0.0f) out << "anchor_up=" << ovr.anchor_up << "\n"; if (ovr.anchor_forward != 0.0f) out << "anchor_forward=" << ovr.anchor_forward << "\n"; out << "anchor_hide=" << (ovr.anchor_hide ? 1 : 0) << "\n"; if (ovr.anchor_rotation != AnchorRotationMode::Off) { out << "anchor_rotation=" << (ovr.anchor_rotation == AnchorRotationMode::Full ? "full" : "yaw") << "\n"; } if (ovr.anchor_yaw_deg != 0.0f) out << "anchor_yaw=" << ovr.anchor_yaw_deg << "\n"; if (ovr.anchor_units_per_meter > 0.0f) out << "anchor_upm=" << ovr.anchor_units_per_meter << "\n"; } if (ovr.handling == HandlingType::ControllerAnchor) { out << "anchor_hand=" << (ovr.anchor_hand == 0 ? "left" : "right") << "\n"; if (ovr.anchor_right != 0.0f) out << "anchor_right=" << ovr.anchor_right << "\n"; if (ovr.anchor_up != 0.0f) out << "anchor_up=" << ovr.anchor_up << "\n"; if (ovr.anchor_forward != 0.0f) out << "anchor_forward=" << ovr.anchor_forward << "\n"; if (ovr.anchor_rotation != AnchorRotationMode::Off) out << "anchor_rotation=full\n"; if (ovr.anchor_yaw_deg != 0.0f) out << "anchor_yaw=" << ovr.anchor_yaw_deg << "\n"; if (ovr.anchor_pitch_deg != 0.0f) out << "anchor_pitch=" << ovr.anchor_pitch_deg << "\n"; if (ovr.anchor_roll_deg != 0.0f) out << "anchor_roll=" << ovr.anchor_roll_deg << "\n"; } if (!ovr.flag_group.empty()) out << "flag=" << ovr.flag_group << "\n"; if (!ovr.condition_flag.empty()) { out << "condition=" << ovr.condition_flag << "\n"; out << "condition_mode=" << (ovr.condition_inverted ? "deactivate" : "activate") << "\n"; } out << "family_version=" << ovr.family_version << "\n"; if (ovr.hash_family_match) out << "hash_family=1\n"; // Persist the family signature whenever we have one (even for exact-hash entries) so the // entry can be switched to family matching later without the game running. if (ovr.family_signature != 0) out << "family_signature=" << fmt::format("{:016x}", ovr.family_signature) << "\n"; if (!ovr.texture_hashes.empty()) { out << "texture_mode=" << (ovr.texture_hashes_excluded ? "exclude" : "include") << "\n"; for (u64 texture_hash : ovr.texture_hashes) out << "texture=" << fmt::format("{:016x}", texture_hash) << "\n"; } out << "\n"; } std::ofstream outfile(path, std::ios::trunc); outfile << out.str(); } // ============================================================================ // Runtime override management // ============================================================================ void ShaderHunter::LoadOverrides(const std::string& game_id) { std::lock_guard lock(m_mutex); m_overrides.clear(); m_override_ps_hashes.clear(); m_override_vs_hashes.clear(); m_override_gs_hashes.clear(); m_screen_hashes.clear(); m_fullscreen_hashes.clear(); m_headlocked_hashes.clear(); m_units_per_meter_overrides.clear(); m_element_depths.clear(); m_passthrough_opacities.clear(); m_camera_anchors.clear(); m_controller_anchors.clear(); m_flag_rules.clear(); m_conditional_overrides.clear(); m_flags_seen_this_frame.clear(); m_flag_age.clear(); m_all_override_hashes.clear(); m_has_texture_overrides = false; m_loaded_game_id = game_id; m_has_overrides.store(false, std::memory_order_relaxed); m_needs_shader_family_signatures.store(false, std::memory_order_relaxed); m_needs_texture_hashes.store(false, std::memory_order_relaxed); if (game_id.empty()) return; auto all = LoadOverridesFromINI(game_id); bool has_overrides = false; bool needs_shader_family_signatures = false; bool needs_texture_hashes = false; for (auto& ovr : all) { if (!ovr.enabled) continue; has_overrides = true; if (ovr.hash_family_match) needs_shader_family_signatures = true; if (!ovr.texture_hashes.empty()) needs_texture_hashes = true; m_all_override_hashes.insert(ovr.hash); // Any override with a flag_group sets a flag when drawn (regardless of handling type) if (!ovr.flag_group.empty()) m_flag_rules.push_back({ovr.type, ovr.hash, ovr.hash_family_match, ovr.family_signature, ovr.family_version, ovr.flag_group, ovr.texture_hashes, ovr.texture_hashes_excluded}); // Flag-only overrides: just set the flag, render normally (no handling change) if (ovr.handling == HandlingType::Flag) { m_overrides.push_back(std::move(ovr)); continue; } // Conditional or texture-conditioned overrides: store separately if (!ovr.condition_flag.empty() || !ovr.texture_hashes.empty() || ovr.hash_family_match) { if (!ovr.texture_hashes.empty()) m_has_texture_overrides = true; m_conditional_overrides.push_back( {ovr.hash, ovr.handling, ovr.type, ovr.element_depth, ovr.units_per_meter, ovr.passthrough_opacity, MakeCameraAnchorParams(ovr), MakeControllerAnchorParams(ovr), ovr.texture_hashes, ovr.texture_hashes_excluded, ovr.hash_family_match, ovr.family_signature, ovr.family_version, ovr.condition_flag, ovr.condition_inverted}); m_overrides.push_back(std::move(ovr)); continue; } // Unconditional overrides: insert into fast hash sets (existing behavior) switch (ovr.handling) { case HandlingType::Skip: switch (ovr.type) { case ShaderType::Pixel: m_override_ps_hashes.insert(ovr.hash); break; case ShaderType::Vertex: m_override_vs_hashes.insert(ovr.hash); break; case ShaderType::Geometry: m_override_gs_hashes.insert(ovr.hash); break; default: break; } break; case HandlingType::Screen: m_screen_hashes.insert(ovr.hash); if (ovr.element_depth >= 0.0f) m_element_depths[ovr.hash] = ovr.element_depth; break; case HandlingType::Fullscreen: m_fullscreen_hashes.insert(ovr.hash); break; case HandlingType::FullscreenMono: m_fullscreen_hashes.insert(ovr.hash); break; case HandlingType::HeadLocked: m_headlocked_hashes.insert(ovr.hash); if (ovr.element_depth >= 0.0f) m_element_depths[ovr.hash] = ovr.element_depth; break; case HandlingType::UnitsPerMeter: if (ovr.units_per_meter > 0.0f) m_units_per_meter_overrides[ovr.hash] = ovr.units_per_meter; break; case HandlingType::Passthrough: m_passthrough_opacities[ovr.hash] = std::clamp(ovr.passthrough_opacity, 0.0f, 1.0f); break; case HandlingType::CameraAnchor: m_camera_anchors[ovr.hash] = MakeCameraAnchorParams(ovr); break; case HandlingType::ControllerAnchor: m_controller_anchors[ovr.hash] = MakeControllerAnchorParams(ovr); break; default: break; } m_overrides.push_back(std::move(ovr)); } m_has_overrides.store(has_overrides, std::memory_order_relaxed); m_needs_shader_family_signatures.store(needs_shader_family_signatures, std::memory_order_relaxed); m_needs_texture_hashes.store(needs_texture_hashes, std::memory_order_relaxed); if (!m_overrides.empty()) { INFO_LOG_FMT(VIDEO, "ShaderHunter: Loaded {} enabled shader overrides for game {}", m_overrides.size(), game_id); } } void ShaderHunter::LoadOverridesIfNeeded(const std::string& game_id) { if (game_id == m_loaded_game_id) return; LoadOverrides(game_id); } void ShaderHunter::AddAndSaveOverride(const std::string& game_id, const std::string& name, ShaderType type, u64 hash, HandlingType handling) { // Load all existing overrides from INI (including disabled ones) auto all = LoadOverridesFromINI(game_id); // Add the new override (enabled by default) ShaderOverride entry; entry.name = name; entry.hash = hash; entry.type = type; entry.handling = handling; entry.enabled = true; entry.user_defined = true; // Prefer family matching so the override survives shader-generator updates; exact hashes embed // code_version and break on every bump. if (const auto family = GetShaderFamilySignature(type, hash)) { entry.hash_family_match = true; entry.match_mode = MatchMode::ShaderFamily; entry.family_signature = *family; } all.push_back(entry); // Save all overrides back to INI, then rebuild the runtime handling sets so the new entry takes // effect immediately (family-mode entries live in the conditional set, not the plain hash sets). SaveOverridesToINI(game_id, all); LoadOverrides(game_id); INFO_LOG_FMT(VIDEO, "ShaderHunter: Saved override '{}' (hash={:016x}, type={}, handling={}) for game {}", name, hash, type == ShaderType::Pixel ? "PS" : type == ShaderType::Vertex ? "VS" : "GS", handling == HandlingType::Screen ? "screen" : handling == HandlingType::Fullscreen || handling == HandlingType::FullscreenMono ? "fullscreen" : handling == HandlingType::HeadLocked ? "headlocked" : handling == HandlingType::Flag ? "flag" : handling == HandlingType::UnitsPerMeter ? "units_per_meter" : handling == HandlingType::Passthrough ? "passthrough" : handling == HandlingType::CameraAnchor ? "camera_anchor" : handling == HandlingType::ControllerAnchor ? "controller_anchor" : "skip", game_id); } bool ShaderHunter::HasOverrides() const { return m_has_overrides.load(std::memory_order_relaxed); } bool ShaderHunter::NeedsShaderFamilySignatures() const { return m_needs_shader_family_signatures.load(std::memory_order_relaxed); } bool ShaderHunter::NeedsTextureHashes() const { return m_needs_texture_hashes.load(std::memory_order_relaxed); } void ShaderHunter::RegisterFlags(u64 vs_hash, u64 ps_hash, u64 gs_hash) { if (m_flag_rules.empty()) return; const auto matches_rule_shader = [&](const FlagRule& rule) { u64 current_hash = 0; u64 current_family = 0; switch (rule.type) { case ShaderType::Vertex: current_hash = vs_hash; current_family = m_current_vs_family; break; case ShaderType::Pixel: current_hash = ps_hash; current_family = m_current_ps_family; break; case ShaderType::Geometry: current_hash = gs_hash; current_family = m_current_gs_family; break; default: return false; } if (rule.hash_family_match) { u64 rule_family = rule.family_signature; // Legacy (scheme v1) VS/GS family signatures were CRC32 over the raw UID bytes — the same // value as the exact shader hash — so compare them against the draw's hash instead. bool legacy_scheme = rule.family_version < FAMILY_SCHEME_VERSION && rule.type != ShaderType::Pixel; if (rule_family == 0) { const auto& per_type = m_shader_family_signatures[static_cast(rule.type)]; const auto it = per_type.find(rule.hash); if (it != per_type.end()) rule_family = it->second; legacy_scheme = false; // live table always holds current-scheme signatures } const u64 comparand = legacy_scheme ? current_hash : current_family; if (rule_family != 0 && comparand != 0) return rule_family == comparand; } return current_hash == rule.hash; }; for (const auto& rule : m_flag_rules) { if (rule.flag_group.empty()) continue; if (!matches_rule_shader(rule)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, rule.texture_hashes, rule.texture_hashes_excluded)) { continue; } m_flags_seen_this_frame.insert(rule.flag_group); } } void ShaderHunter::RegisterExternalFlag(const std::string& flag_name) { if (flag_name.empty()) return; m_flags_seen_this_frame.insert(flag_name); } bool ShaderHunter::IsFlagActive(const std::string& flag_name) const { std::lock_guard lock(m_mutex); if (flag_name.empty()) return false; return m_flag_age.find(flag_name) != m_flag_age.end(); } std::vector ShaderHunter::GetFlagStatusesForOSD() const { std::set known_flags; for (const auto& rule : m_flag_rules) { if (!rule.flag_group.empty()) known_flags.insert(rule.flag_group); } for (const auto& [flag_name, _] : m_flag_age) known_flags.insert(flag_name); for (const auto& ovr : m_overrides) { if (!ovr.condition_flag.empty()) known_flags.insert(ovr.condition_flag); } std::vector result; result.reserve(known_flags.size()); for (const auto& flag_name : known_flags) { FlagStatus status; status.flag_name = flag_name; status.is_active = m_flag_age.find(flag_name) != m_flag_age.end() || m_flags_seen_this_frame.find(flag_name) != m_flags_seen_this_frame.end(); std::unordered_set seen_names; for (const auto& ovr : m_overrides) { if (ovr.condition_flag != flag_name) continue; std::string shader_name = ovr.name; if (shader_name.empty()) shader_name = fmt::format("Unnamed {:08x}", static_cast(ovr.hash & 0xffffffffULL)); if (seen_names.insert(shader_name).second) status.impacted_shader_names.push_back(std::move(shader_name)); } std::sort(status.impacted_shader_names.begin(), status.impacted_shader_names.end()); result.push_back(std::move(status)); } return result; } void ShaderHunter::SetDebugLogging(bool enabled) { m_debug_logging = enabled; if (enabled) INFO_LOG_FMT(VIDEO, "ShaderHunter: Debug override logging ENABLED"); else INFO_LOG_FMT(VIDEO, "ShaderHunter: Debug override logging DISABLED"); } bool ShaderHunter::IsDebugLogging() const { return m_debug_logging; } void ShaderHunter::DebugLogUnmatched(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (!m_debug_logging || m_all_override_hashes.empty()) return; // Only log if at least one of the draw call's hashes appears in any override if (m_all_override_hashes.count(vs_hash) == 0 && m_all_override_hashes.count(ps_hash) == 0 && m_all_override_hashes.count(gs_hash) == 0) return; const u64 combo = vs_hash ^ (ps_hash * 0x9e3779b97f4a7c15ULL) ^ 0x2ULL; if (m_debug_logged_combos.count(combo) != 0) return; m_debug_logged_combos.insert(combo); INFO_LOG_FMT(VIDEO, "ShaderHunter: NO MATCH (default render) VS={:08x} PS={:08x} GS={:08x}", static_cast(vs_hash), static_cast(ps_hash), static_cast(gs_hash)); } bool ShaderHunter::IsConditionFlagMatch(const ConditionalOverride& cond) const { if (cond.condition_flag.empty()) return true; const bool is_active = m_flag_age.count(cond.condition_flag) > 0; return cond.condition_inverted ? !is_active : is_active; } u64 ShaderHunter::ResolveConditionalFamilySignature(const ConditionalOverride& cond, bool* out_legacy_scheme) const { *out_legacy_scheme = false; if (!cond.hash_family_match) return 0; if (cond.family_signature != 0) { // Legacy (scheme v1) VS/GS family signatures were CRC32 over the raw UID bytes — the same // value as the exact shader hash — so they must be compared against the draw's hash rather // than the semantic family. Pixel families were semantic from the start. *out_legacy_scheme = cond.family_version < FAMILY_SCHEME_VERSION && cond.type != ShaderType::Pixel; return cond.family_signature; } // Resolved from the live table, which always holds current-scheme signatures. const auto& per_type = m_shader_family_signatures[static_cast(cond.type)]; const auto it = per_type.find(cond.hash); if (it == per_type.end()) return 0; return it->second; } bool ShaderHunter::IsConditionalHashMatch(const ConditionalOverride& cond, u64 vs_hash, u64 ps_hash, u64 gs_hash) const { u64 current_hash = 0; u64 current_family = 0; switch (cond.type) { case ShaderType::Vertex: current_hash = vs_hash; current_family = m_current_vs_family; break; case ShaderType::Pixel: current_hash = ps_hash; current_family = m_current_ps_family; break; case ShaderType::Geometry: current_hash = gs_hash; current_family = m_current_gs_family; break; default: return false; } if (cond.hash_family_match) { bool legacy_scheme = false; const u64 cond_family = ResolveConditionalFamilySignature(cond, &legacy_scheme); const u64 comparand = legacy_scheme ? current_hash : current_family; if (cond_family != 0 && comparand != 0) return cond_family == comparand; } return current_hash == cond.hash; } bool ShaderHunter::ShouldBypassSelectedOverrideForTextureTool(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (!m_enabled || !m_texture_tool_active.load(std::memory_order_relaxed)) return false; const int active_type_index = static_cast(m_active_type); if (active_type_index < 0 || active_type_index >= TYPE_COUNT) return false; if (m_selected_pos[active_type_index] < 0) return false; const u64 selected_hash = m_selected_hash[active_type_index]; switch (m_active_type) { case ShaderType::Pixel: return ps_hash == selected_hash; case ShaderType::Vertex: return vs_hash == selected_hash; case ShaderType::Geometry: return gs_hash == selected_hash; default: return false; } } bool ShaderHunter::ShouldSkipByOverride(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (m_overrides.empty()) return false; if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return false; // Unconditional skip check (existing O(1) hash sets) if (m_override_ps_hashes.count(ps_hash) > 0 || m_override_vs_hashes.count(vs_hash) > 0 || m_override_gs_hashes.count(gs_hash) > 0) { if (m_debug_logging) { const u64 combo = vs_hash ^ (ps_hash * 0x9e3779b97f4a7c15ULL); if (m_debug_logged_combos.count(combo) == 0) { m_debug_logged_combos.insert(combo); INFO_LOG_FMT(VIDEO, "ShaderHunter: SKIP (unconditional) VS={:08x} PS={:08x} GS={:08x}", static_cast(vs_hash), static_cast(ps_hash), static_cast(gs_hash)); } } return true; } // Conditional/draw-call-range/texture skip overrides for (const auto& cond : m_conditional_overrides) { if (cond.handling != HandlingType::Skip) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; // Texture condition: require at least one matching texture bound to any stage. if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; if (m_debug_logging) { const u64 combo = vs_hash ^ (ps_hash * 0x9e3779b97f4a7c15ULL); if (m_debug_logged_combos.count(combo) == 0) { m_debug_logged_combos.insert(combo); INFO_LOG_FMT(VIDEO, "ShaderHunter: SKIP (conditional) VS={:08x} PS={:08x} GS={:08x} " "matched={:08x} cond='{}{}'", static_cast(vs_hash), static_cast(ps_hash), static_cast(gs_hash), static_cast(cond.hash), cond.condition_inverted ? "!" : "", cond.condition_flag); } } return true; } return false; } ShaderHunter::HandlingType ShaderHunter::GetOverrideHandling(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return HandlingType::Skip; const char* handling_name = nullptr; HandlingType result = HandlingType::Skip; // Unconditional checks (existing O(1) hash sets) if (m_screen_hashes.count(vs_hash) > 0 || m_screen_hashes.count(ps_hash) > 0 || m_screen_hashes.count(gs_hash) > 0) { result = HandlingType::Screen; handling_name = "Screen"; } else if (m_fullscreen_hashes.count(vs_hash) > 0 || m_fullscreen_hashes.count(ps_hash) > 0 || m_fullscreen_hashes.count(gs_hash) > 0) { result = HandlingType::Fullscreen; handling_name = "Fullscreen"; } else if (m_headlocked_hashes.count(vs_hash) > 0 || m_headlocked_hashes.count(ps_hash) > 0 || m_headlocked_hashes.count(gs_hash) > 0) { result = HandlingType::HeadLocked; handling_name = "HeadLocked"; } else if (m_units_per_meter_overrides.count(vs_hash) > 0 || m_units_per_meter_overrides.count(ps_hash) > 0 || m_units_per_meter_overrides.count(gs_hash) > 0) { result = HandlingType::UnitsPerMeter; handling_name = "UnitsPerMeter"; } else if (m_passthrough_opacities.count(vs_hash) > 0 || m_passthrough_opacities.count(ps_hash) > 0 || m_passthrough_opacities.count(gs_hash) > 0) { result = HandlingType::Passthrough; handling_name = "Passthrough"; } else if (m_camera_anchors.count(vs_hash) > 0 || m_camera_anchors.count(ps_hash) > 0 || m_camera_anchors.count(gs_hash) > 0) { result = HandlingType::CameraAnchor; handling_name = "CameraAnchor"; } else if (m_controller_anchors.count(vs_hash) > 0 || m_controller_anchors.count(ps_hash) > 0 || m_controller_anchors.count(gs_hash) > 0) { result = HandlingType::ControllerAnchor; handling_name = "ControllerAnchor"; } else { // Conditional/draw-call-range/texture overrides (small vector scan) for (const auto& cond : m_conditional_overrides) { if (cond.handling == HandlingType::Skip) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; // Texture condition: require at least one matching texture bound to any stage. if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; result = cond.handling; handling_name = cond.handling == HandlingType::Screen ? "Screen(conditional)" : cond.handling == HandlingType::Fullscreen || cond.handling == HandlingType::FullscreenMono ? "Fullscreen(conditional)" : cond.handling == HandlingType::HeadLocked ? "HeadLocked(conditional)" : cond.handling == HandlingType::UnitsPerMeter ? "UnitsPerMeter(conditional)" : cond.handling == HandlingType::Passthrough ? "Passthrough(conditional)" : cond.handling == HandlingType::CameraAnchor ? "CameraAnchor(conditional)" : "???"; break; } } if (m_debug_logging && handling_name != nullptr) { const u64 combo = vs_hash ^ (ps_hash * 0x9e3779b97f4a7c15ULL) ^ 0x1ULL; if (m_debug_logged_combos.count(combo) == 0) { m_debug_logged_combos.insert(combo); INFO_LOG_FMT(VIDEO, "ShaderHunter: {} VS={:08x} PS={:08x} GS={:08x}", handling_name, static_cast(vs_hash), static_cast(ps_hash), static_cast(gs_hash)); } } return result; } float ShaderHunter::GetOverrideElementDepth(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return -1.0f; // Unconditional element depths for (u64 h : {vs_hash, ps_hash, gs_hash}) { auto it = m_element_depths.find(h); if (it != m_element_depths.end()) return it->second; } // Conditional element depths for (const auto& cond : m_conditional_overrides) { if (cond.element_depth < 0.0f) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; return cond.element_depth; } return -1.0f; // use global } float ShaderHunter::GetOverrideUnitsPerMeter(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return -1.0f; // Unconditional per-hash UPM overrides for (u64 h : {vs_hash, ps_hash, gs_hash}) { auto it = m_units_per_meter_overrides.find(h); if (it != m_units_per_meter_overrides.end() && it->second > 0.0f) return it->second; } // Conditional per-hash UPM overrides for (const auto& cond : m_conditional_overrides) { if (cond.handling != HandlingType::UnitsPerMeter || cond.units_per_meter <= 0.0f) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; return cond.units_per_meter; } return -1.0f; // use global } float ShaderHunter::GetOverridePassthroughOpacity(u64 vs_hash, u64 ps_hash, u64 gs_hash) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return 0.0f; // Unconditional per-hash Passthrough overrides for (u64 h : {vs_hash, ps_hash, gs_hash}) { auto it = m_passthrough_opacities.find(h); if (it != m_passthrough_opacities.end()) return it->second; } // Conditional per-hash Passthrough overrides for (const auto& cond : m_conditional_overrides) { if (cond.handling != HandlingType::Passthrough) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; return std::clamp(cond.passthrough_opacity, 0.0f, 1.0f); } return 0.0f; // fully see-through } bool ShaderHunter::GetOverrideControllerAnchor(u64 vs_hash, u64 ps_hash, u64 gs_hash, ControllerAnchorParams* out_params) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return false; // Unconditional per-hash ControllerAnchor overrides for (u64 h : {vs_hash, ps_hash, gs_hash}) { auto it = m_controller_anchors.find(h); if (it != m_controller_anchors.end()) { *out_params = it->second; return true; } } // Conditional per-hash ControllerAnchor overrides for (const auto& cond : m_conditional_overrides) { if (cond.handling != HandlingType::ControllerAnchor) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; *out_params = cond.controller_anchor; return true; } return false; } bool ShaderHunter::GetOverrideCameraAnchor(u64 vs_hash, u64 ps_hash, u64 gs_hash, CameraAnchorParams* out_params) const { if (ShouldBypassSelectedOverrideForTextureTool(vs_hash, ps_hash, gs_hash)) return false; // Unconditional per-hash CameraAnchor overrides for (u64 h : {vs_hash, ps_hash, gs_hash}) { auto it = m_camera_anchors.find(h); if (it != m_camera_anchors.end()) { *out_params = it->second; return true; } } // Conditional per-hash CameraAnchor overrides for (const auto& cond : m_conditional_overrides) { if (cond.handling != HandlingType::CameraAnchor) continue; if (!IsConditionalHashMatch(cond, vs_hash, ps_hash, gs_hash)) continue; if (!IsConditionFlagMatch(cond)) continue; if (!DoesTextureFilterPass(m_current_draw_textures, cond.texture_hashes, cond.texture_hashes_excluded)) continue; *out_params = cond.anchor; return true; } return false; } // ============================================================================ // Shader dumping — regenerate source from cached UID and write to file // ============================================================================ bool ShaderHunter::DumpShader(const std::string& game_id, ShaderType type, u64 hash) const { std::lock_guard lock(m_mutex); const int t = static_cast(type); auto it = m_uid_cache[t].find(hash); if (it == m_uid_cache[t].end()) { WARN_LOG_FMT(VIDEO, "ShaderHunter: No cached UID for hash {:08x}, cannot dump", hash); return false; } const auto& uid_bytes = it->second; const APIType api_type = g_backend_info.api_type; const ShaderHostConfig host_config = ShaderHostConfig::GetCurrent(); std::string source; const char* type_suffix = ""; switch (type) { case ShaderType::Vertex: { if (uid_bytes.size() < sizeof(vertex_shader_uid_data)) { WARN_LOG_FMT(VIDEO, "ShaderHunter: UID data too small for VS (got {}, need {})", uid_bytes.size(), sizeof(vertex_shader_uid_data)); return false; } const auto* uid_data = reinterpret_cast(uid_bytes.data()); ShaderCode code = GenerateVertexShaderCode(api_type, host_config, uid_data, {}); source = code.GetBuffer(); type_suffix = "vs"; break; } case ShaderType::Pixel: { if (uid_bytes.size() < sizeof(pixel_shader_uid_data)) { WARN_LOG_FMT(VIDEO, "ShaderHunter: UID data too small for PS (got {}, need {})", uid_bytes.size(), sizeof(pixel_shader_uid_data)); return false; } const auto* uid_data = reinterpret_cast(uid_bytes.data()); ShaderCode code = GeneratePixelShaderCode(api_type, host_config, uid_data, {}); source = code.GetBuffer(); type_suffix = "ps"; break; } case ShaderType::Geometry: { if (uid_bytes.size() < sizeof(geometry_shader_uid_data)) { WARN_LOG_FMT(VIDEO, "ShaderHunter: UID data too small for GS (got {}, need {})", uid_bytes.size(), sizeof(geometry_shader_uid_data)); return false; } const auto* uid_data = reinterpret_cast(uid_bytes.data()); ShaderCode code = GenerateGeometryShaderCode(api_type, host_config, uid_data); source = code.GetBuffer(); type_suffix = "gs"; break; } default: return false; } // Build dump path: Dump/Shaders// const std::string dump_dir = File::GetUserPath(D_DUMP_IDX) + "Shaders/" + game_id + "/"; if (!File::IsDirectory(dump_dir)) File::CreateFullPath(dump_dir); const std::string filename = dump_dir + fmt::format("{:08x}-{}.txt", static_cast(hash), type_suffix); std::ofstream outfile(filename, std::ios::trunc); if (!outfile.is_open()) { WARN_LOG_FMT(VIDEO, "ShaderHunter: Failed to open '{}' for writing", filename); return false; } outfile << source; outfile.close(); INFO_LOG_FMT(VIDEO, "ShaderHunter: Dumped {} shader {:08x} to '{}'", type_suffix, static_cast(hash), filename); return true; } std::vector ShaderHunter::GetTexturesForSelectedShader() const { std::lock_guard lock(m_mutex); std::vector result; result.reserve(m_texture_usage_display.size()); for (const auto& [texture_hash, texture_name] : m_texture_usage_display) result.push_back({texture_hash, texture_name}); std::sort(result.begin(), result.end(), [](const TextureUsage& a, const TextureUsage& b) { return a.hash < b.hash; }); return result; } std::vector ShaderHunter::GetTexturesForHash(ShaderType type, u64 hash) const { std::lock_guard lock(m_mutex); std::vector result; const auto& per_type = m_shader_texture_usage_display[static_cast(type)]; auto it = per_type.find(hash); if (it == per_type.end()) return result; result.reserve(it->second.size()); for (const auto& [texture_hash, texture_name] : it->second) result.push_back({texture_hash, texture_name}); std::sort(result.begin(), result.end(), [](const TextureUsage& a, const TextureUsage& b) { return a.hash < b.hash; }); return result; } void ShaderHunter::SetTextureSkipEnabled(u64 texture_hash, bool enabled) { if (texture_hash == 0) return; std::lock_guard lock(m_mutex); m_texture_skip_mode_active = true; if (enabled) m_texture_skip_filters.insert(texture_hash); else m_texture_skip_filters.erase(texture_hash); } bool ShaderHunter::IsTextureSkipEnabled(u64 texture_hash) const { std::lock_guard lock(m_mutex); return m_texture_skip_filters.count(texture_hash) > 0; } void ShaderHunter::ClearTextureSkipFilters() { std::lock_guard lock(m_mutex); m_texture_skip_filters.clear(); m_texture_skip_mode_active = true; } void ShaderHunter::SetTextureToolActive(bool active) { m_texture_tool_active.store(active, std::memory_order_relaxed); } void ShaderHunter::PrevTextureHash() { std::lock_guard lock(m_mutex); const auto hashes = GetSortedTextureHashes(m_texture_usage_display); if (hashes.empty()) { m_selected_texture_hash = 0; return; } auto it = std::find(hashes.begin(), hashes.end(), m_selected_texture_hash); if (it == hashes.end()) { m_selected_texture_hash = hashes.back(); return; } if (it == hashes.begin()) m_selected_texture_hash = hashes.back(); else m_selected_texture_hash = *std::prev(it); } void ShaderHunter::NextTextureHash() { std::lock_guard lock(m_mutex); const auto hashes = GetSortedTextureHashes(m_texture_usage_display); if (hashes.empty()) { m_selected_texture_hash = 0; return; } auto it = std::find(hashes.begin(), hashes.end(), m_selected_texture_hash); if (it == hashes.end()) { m_selected_texture_hash = hashes.front(); return; } ++it; m_selected_texture_hash = (it == hashes.end()) ? hashes.front() : *it; } u64 ShaderHunter::GetSelectedTextureHash() const { std::lock_guard lock(m_mutex); const auto hashes = GetSortedTextureHashes(m_texture_usage_display); if (hashes.empty()) return 0; if (std::find(hashes.begin(), hashes.end(), m_selected_texture_hash) == hashes.end()) return hashes.front(); return m_selected_texture_hash; } bool ShaderHunter::ToggleSelectedTextureHashFilter() { std::lock_guard lock(m_mutex); const auto hashes = GetSortedTextureHashes(m_texture_usage_display); if (hashes.empty()) { m_selected_texture_hash = 0; return false; } if (std::find(hashes.begin(), hashes.end(), m_selected_texture_hash) == hashes.end()) m_selected_texture_hash = hashes.front(); m_texture_skip_mode_active = true; if (m_texture_skip_filters.erase(m_selected_texture_hash) > 0) return false; m_texture_skip_filters.insert(m_selected_texture_hash); return true; } ShaderHunter::HuntingStatus ShaderHunter::GetHuntingStatusForOSD() const { std::lock_guard lock(m_mutex); HuntingStatus status; status.enabled = m_enabled; status.option = m_hunting_option; status.active_type = m_active_type; const int t = static_cast(m_active_type); status.selected_hash = m_selected_hash[t]; status.selected_position = m_selected_pos[t]; status.selected_total = static_cast(m_display[t].size()); const auto texture_hashes = GetSortedTextureHashes(m_texture_usage_display); if (!texture_hashes.empty()) { status.selected_texture_hash = std::find(texture_hashes.begin(), texture_hashes.end(), m_selected_texture_hash) == texture_hashes.end() ? texture_hashes.front() : m_selected_texture_hash; } status.texture_filters.reserve(m_texture_skip_filters.size()); for (const u64 hash : m_texture_skip_filters) status.texture_filters.push_back(hash); std::sort(status.texture_filters.begin(), status.texture_filters.end()); return status; } std::optional ShaderHunter::GetSelectedRuntimeElementSignature() const { std::lock_guard lock(m_mutex); if (!m_selected_draw_signature.valid) return std::nullopt; return m_selected_draw_signature; } bool ShaderHunter::SaveSelectedShaderOverride(const std::string& game_id, HandlingType handling) { ShaderType type = ShaderType::Pixel; u64 hash = 0; u64 texture_hash = 0; { std::lock_guard lock(m_mutex); const int t = static_cast(m_active_type); if (game_id.empty() || m_selected_pos[t] < 0 || m_selected_hash[t] == ~0ULL) return false; type = m_active_type; hash = m_selected_hash[t]; texture_hash = m_selected_texture_hash; } auto all = LoadOverridesFromINI(game_id); ShaderOverride entry; entry.name = fmt::format("Shader {:08x}", static_cast(hash)); entry.hash = hash; entry.type = type; entry.handling = handling; entry.enabled = true; entry.user_defined = true; if (handling == HandlingType::Flag) entry.flag_group = entry.name; if (handling == HandlingType::UnitsPerMeter) entry.units_per_meter = g_Config.vr_units_per_meter; if (texture_hash != 0) entry.texture_hashes.push_back(texture_hash); // Prefer family matching so the override survives shader-generator updates; exact hashes embed // code_version and break on every bump. if (const auto family = GetShaderFamilySignature(type, hash)) { entry.hash_family_match = true; entry.match_mode = MatchMode::ShaderFamily; entry.family_signature = *family; } all.push_back(entry); SaveOverridesToINI(game_id, all); LoadOverrides(game_id); return true; }