// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include #include #include #include #include #include "VideoCommon/MetroidElementClassifier.h" #include "VideoCommon/ShaderHunter.h" class ElementsGroupManager { public: using RuntimeElementSignature = ShaderHunter::RuntimeElementSignature; using ShaderType = ShaderHunter::ShaderType; using HandlingType = ShaderHunter::HandlingType; using HuntingOption = ShaderHunter::HuntingOption; using CameraAnchorParams = ShaderHunter::CameraAnchorParams; using ControllerAnchorParams = ShaderHunter::ControllerAnchorParams; enum class PreviewAction { None = 0, Skip = 1, Pink = 2 }; enum class MatchKind { RuntimeSignature = 0, ProfileLayer = 1, }; enum class ScreenPaneDepthMode { Game = 0, VR = 1, Flat = 2, }; struct DrawRecord { int draw_index = -1; u32 draw_sequence = 0; u64 vs_hash = 0; u64 ps_hash = 0; u64 gs_hash = 0; u64 vs_family = 0; u64 ps_family = 0; u64 gs_family = 0; RuntimeElementSignature signature; MetroidElementProfile profile_id = MetroidElementProfile::None; MetroidElementLayer profile_layer = MetroidElementLayer::Unknown; std::string profile_layer_name; std::array textures{}; std::array texture_names{}; u64 GetHash(ShaderType type) const; u64 GetFamily(ShaderType type) const; }; struct StableSubMatchSignature { RuntimeElementSignature runtime_element; u64 vs_family = 0; u64 ps_family = 0; u64 gs_family = 0; std::vector texture_hashes; int occurrence_slot = -1; }; struct SelectedSubgroupSignature { u64 vs_family = 0; u64 ps_family = 0; u64 gs_family = 0; std::vector texture_hashes; // Scheme of the family values (see ShaderHunter::FAMILY_SCHEME_VERSION). Filters from pre-v6 // INIs are version 1: their vs/gs families are raw-UID CRC32s (equal to the exact shader // hashes) and are matched against draw hashes for backward compatibility. Not part of // signature equality. u32 family_version = ShaderHunter::FAMILY_SCHEME_VERSION; }; struct CurrentMatchCandidate { SelectedSubgroupSignature subgroup; DrawRecord representative_draw; int raw_draw_count = 0; bool active_this_frame = false; }; struct ElementGroupOverride { std::string name; std::string comments; std::string credits; MatchKind match_kind = MatchKind::RuntimeSignature; HandlingType handling = HandlingType::Skip; // Fullscreen handling: preserve separate eye layers when a one-layer texture was partially // updated from a stereo EFB copy. Opt-in because most fullscreen effects are intentionally // mono and do not need the extra texture allocation/copies. bool preserve_stereo_efb = false; RuntimeElementSignature runtime_element; MetroidElementProfile profile_id = MetroidElementProfile::None; std::vector profile_layers; float element_depth = -1.0f; float units_per_meter = -1.0f; // ScreenPane handling: preserve the game's depth buffer values by default, or use the // physically projected VR depth of the re-anchored pane geometry with shared Z composition. ScreenPaneDepthMode screen_pane_depth = ScreenPaneDepthMode::Game; float passthrough_opacity = 0.0f; // Passthrough handling: element opacity (0 = fully camera) // CameraAnchor handling: camera-space offset from the anchor element's origin (meters, // right/up/forward, converted with units-per-meter and mapped to view-space axes at capture // time) and whether to hide the anchor element itself (e.g. the head mesh in first person). float anchor_right = 0.0f; float anchor_up = 0.0f; float anchor_forward = 0.0f; bool anchor_hide = true; // How much of the element's orientation the camera follows, and a fixed yaw correction in // degrees for models whose forward axis is not the assumed object +Z (e.g. 180 = view was // facing backward). ShaderHunter::AnchorRotationMode anchor_rotation = ShaderHunter::AnchorRotationMode::Off; float anchor_yaw_deg = 0.0f; // World scale to use while this anchor is active, so a first-person view can have its own // scale without disturbing the game's global Units per Meter (-1 = keep the global value). float anchor_units_per_meter = -1.0f; // ControllerAnchor handling: which VR controller the element follows (0 = left, 1 = right). // Reuses anchor_right/up/forward as meter offsets from the controller's aim pose and // anchor_rotation (Off/Full) for orientation follow; yaw/pitch/roll are the model-axis // correction in the controller's frame (models differ in which way they natively point). int anchor_hand = 1; float anchor_pitch_deg = 0.0f; float anchor_roll_deg = 0.0f; std::vector texture_hashes; bool texture_hashes_excluded = false; std::vector selected_match_filter; bool selected_match_filter_excluded = false; bool enabled = true; bool user_defined = true; std::string flag_group; std::string condition_flag; bool condition_inverted = false; // Clear EFB: when a draw matching this override is rendered, clear the next EFB-to-texture copy // to transparent instead of copying. Independent of handling (can combine with Skip/Screen/etc.). // Because matching is on the full element signature, only this element arms the clear. bool clear_efb = false; int clear_efb_min_width = 0; // Only clear copies with native width >= this (0 = any). int clear_efb_max_width = 0; // Only clear copies with native width <= this (0 = any). }; struct SeedCandidate { RuntimeElementSignature signature; RuntimeElementSignature group_signature; DrawRecord representative_draw; int occurrence_count = 0; }; struct Status { bool popup_open = false; bool hunt_enabled = false; HuntingOption option = HuntingOption::Skip; bool seed_valid = false; RuntimeElementSignature seed_signature; std::optional seed_draw; int selected_seed_index = -1; int total_seed_candidates = 0; int selected_match = 0; int total_matches = 0; int active_matches = 0; int selected_match_filter_count = 0; bool selected_match_filter_excluded = false; int highlighted_match_raw_draw_count = 0; std::optional highlighted_draw; }; static ElementsGroupManager& GetInstance(); static std::vector LoadOverridesFromINI( const std::string& game_id, std::optional revision = std::nullopt); static void SaveOverridesToINI(const std::string& game_id, const std::vector& overrides); static RuntimeElementSignature MakeSelectedMatchFilterSignature( const RuntimeElementSignature& signature); static StableSubMatchSignature MakeStableSubMatchSignature(const DrawRecord& draw, int occurrence_slot); static SelectedSubgroupSignature MakeSelectedSubgroupSignature(const DrawRecord& draw); void LoadOverrides(const std::string& game_id); void LoadOverridesIfNeeded(const std::string& game_id); bool HasOverrides() const; void SetPopupOpen(bool open); bool IsPopupOpen() const; void SetHuntEnabled(bool enabled); bool IsHuntEnabled() const; void SetHuntingOption(HuntingOption option); HuntingOption GetHuntingOption() const; void SelectSeedCandidate(int index); void SetSeedGroupMask(bool projection, bool layer, bool viewport, bool scissor, bool render_state); void SelectMatch(int index); void NextMatch(); void PrevMatch(); std::vector GetSeedCandidates() const; std::vector GetCurrentMatches() const; std::vector GetSelectedMatchFilters() const; bool GetSelectedMatchFilterExcluded() const; std::vector GetSelectedMatchDisplayDraws() const; std::vector GetCurrentTextureSourceDraws() const; std::optional GetHighlightedDraw() const; std::optional GetSelectedSeedDraw() const; std::optional GetSelectedCurrentMatchDraw() const; std::optional GetCurrentTextureSourceDraw() const; RuntimeElementSignature GetSeedSignature() const; Status GetStatus() const; bool IsCurrentMatchFilterEnabled(int match_index) const; void SetSelectedMatchFilterExcluded(bool excluded); void SetCurrentMatchFilterEnabled(int match_index, bool enabled); void AddCurrentMatchFilter(); void RemoveSelectedMatchFilter(int index); PreviewAction RegisterDraw(const DrawRecord& draw); void OnFrameEnd(); void RegisterFlagsForDraw(const DrawRecord& draw); bool ShouldSkipByOverride(const DrawRecord& draw) const; HandlingType GetOverrideHandling(const DrawRecord& draw, bool* out_preserve_stereo_efb = nullptr) const; ScreenPaneDepthMode GetOverrideScreenPaneDepth(const DrawRecord& draw, u64* out_group_id = nullptr) const; float GetOverrideElementDepth(const DrawRecord& draw) const; float GetOverrideUnitsPerMeter(const DrawRecord& draw) const; // Returns the opacity for a Passthrough override (0 = fully see-through to the camera). float GetOverridePassthroughOpacity(const DrawRecord& draw) const; // Fills the parameters of the first matching CameraAnchor override. // Returns false when none matches the draw. bool GetOverrideCameraAnchor(const DrawRecord& draw, CameraAnchorParams* out_params) const; // Fills the parameters of the first matching ControllerAnchor override. // Returns false when none matches the draw. bool GetOverrideControllerAnchor(const DrawRecord& draw, ControllerAnchorParams* out_params) const; // Clear EFB: arm a pending clear when a draw matches a clear_efb override; ShouldClearEFBCopy is // consumed by the next EFB-to-texture copy (see TextureCacheBase). void CheckClearEFBForDraw(const DrawRecord& draw); bool ShouldClearEFBCopy(int width); private: ElementsGroupManager() = default; struct StableSubMatchContext { bool valid = false; u32 draw_sequence = 0; StableSubMatchSignature signature; }; struct GroupMask { bool projection = false; bool layer = false; bool viewport = false; bool scissor = false; bool render_state = false; }; void ClearSeedSelectionLocked(); void SelectSeedCandidateLocked(int index); bool HasActiveHuntLocked() const; RuntimeElementSignature GetMaskedSeedSignatureLocked() const; RuntimeElementSignature GetSeedGroupSignatureLocked(const RuntimeElementSignature& signature) const; bool MatchesSelectedMatchFilterLocked(const DrawRecord& draw) const; bool MatchesSelectedMatchFilterSignatureLocked(const SelectedSubgroupSignature& signature) const; std::vector ResolveSelectedMatchDisplayDrawsLocked() const; StableSubMatchSignature GetStableSubMatchSignatureLocked(const DrawRecord& draw) const; bool DoesEntryBaseMatch(const ElementGroupOverride& entry, const DrawRecord& draw) const; bool DoesSelectedMatchFilterPass(const ElementGroupOverride& entry, const DrawRecord& draw) const; bool DoesEntryMatch(const ElementGroupOverride& entry, const DrawRecord& draw, bool include_condition) const; bool DoesTextureFilterPass(const DrawRecord& draw, const ElementGroupOverride& entry) const; mutable std::mutex m_mutex; int m_popup_open_count = 0; std::atomic_bool m_popup_open = false; std::atomic_bool m_has_overrides = false; bool m_hunt_enabled = false; HuntingOption m_hunting_option = HuntingOption::Skip; RuntimeElementSignature m_seed_signature{}; RuntimeElementSignature m_seed_group_signature{}; std::optional m_seed_draw; GroupMask m_group_mask; int m_selected_seed_index = -1; int m_selected_match = 0; std::vector m_selected_match_filters; bool m_selected_match_filters_excluded = false; std::vector m_collecting_draws; std::vector m_display_draws; std::vector m_display_seed_candidates; std::vector m_collecting_matches; std::vector m_display_raw_matches; std::vector m_display_raw_match_signatures; std::vector m_display_matches; int m_collecting_match_total = 0; int m_display_match_total = 0; std::optional m_collecting_highlighted_draw; std::optional m_display_highlighted_draw; int m_display_highlighted_match_raw_draw_count = 0; std::vector m_overrides; std::string m_loaded_game_id; mutable std::unordered_map m_stable_submatch_occurrence_counters; mutable StableSubMatchContext m_current_stable_submatch; // Clear EFB pending state (video thread): armed by CheckClearEFBForDraw, consumed by the next // ShouldClearEFBCopy. bool m_clear_next_efb = false; int m_pending_clear_min = 0; int m_pending_clear_max = 0; };