10 Commits
Author SHA1 Message Date
Christophe 339a7ed52d Readme 2026-04-12 02:34:41 +02:00
Christophe 220a99b7a3 Readme 2026-04-12 02:32:04 +02:00
iChris4 49febf2cbe WII Controller Config Screen Overlay for OpenXR & Quest 3 Controller Fix 2026-04-12 02:08:02 +02:00
iChris4 95b569b681 WIP CullingCodeFinder 2026-04-11 18:11:02 +02:00
iChris4 677aa34050 Added ARMode by Clodo76 2026-04-11 18:08:04 +02:00
iChris4 4a70fd2fbe VR Pane UI 2026-04-10 19:51:45 +02:00
iChris4 1c16f73855 VRPane Settings 2026-04-09 02:11:22 +02:00
iChris4 2ed53222b4 Update Readme 2026-04-07 22:32:00 +02:00
iChris4 780b79b8d7 Add OpenXR as submodule 2026-04-07 00:37:56 +02:00
iChris4 a2dad38be5 Add OpenXR support and related VR changes 2026-04-07 00:18:01 +02:00
3137 changed files with 586348 additions and 246 deletions

No files matched your search

+10
View File
@@ -93,6 +93,7 @@ option(ENABLE_CUBEB "Enables Cubeb sound backend" ON)
option(ENABLE_LLVM "Enables LLVM support, for disassembly" ON)
option(ENABLE_TESTS "Enables building the unit tests" ON)
option(ENABLE_VULKAN "Enables vulkan video backend" ON)
option(ENABLE_VR "Enables VR support via OpenXR (Windows, D3D11 + Vulkan)" OFF)
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current game on Discord" ON)
option(USE_MGBA "Enables GBA controllers emulation using libmgba" ON)
option(ENABLE_AUTOUPDATE "Enables support for automatic updates" ON)
@@ -653,6 +654,15 @@ if(ENABLE_VULKAN)
endif()
endif()
if(ENABLE_VR AND WIN32)
add_definitions(-DENABLE_VR)
# Build only the OpenXR loader from the Khronos SDK; skip tests, conformance, and samples.
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE)
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
add_subdirectory(Externals/OpenXR "${CMAKE_BINARY_DIR}/Externals/OpenXR" EXCLUDE_FROM_ALL)
endif()
if(NOT (WIN32 AND _M_ARM_64))
# OpenGL is available on all platforms except windows-arm64
add_definitions(-DHAS_OPENGL)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Vendored Submodule
+1
Submodule Externals/OpenXR added at 472d817ffe.
+2
View File
@@ -3,6 +3,8 @@ set(MZ_BUILD_TESTS OFF)
set(MZ_BUILD_UNIT_TESTS OFF)
set(MZ_BUILD_FUZZ_TESTS OFF)
set(MZ_CODE_COVERAGE OFF)
set(MZ_FETCH_LIBS OFF CACHE BOOL "Disable minizip-ng vendored dependency fetch/build." FORCE)
set(MZ_FORCE_FETCH_LIBS OFF CACHE BOOL "Disable minizip-ng forced vendored dependency fetch/build." FORCE)
set(SKIP_INSTALL_ALL ON)
add_subdirectory(minizip-ng)
+27
View File
@@ -6,6 +6,9 @@ Dolphin is an emulator for running GameCube and Wii games on Windows,
Linux, macOS, and recent Android devices. It's licensed under the terms
of the GNU General Public License, version 2 or later (GPLv2+).
This fork adds OpenXR support to Dolphin so users can play in VR on supported
Windows builds and was developed with the use of AI tools.
Please read the [FAQ](https://dolphin-emu.org/docs/faq/) before using Dolphin.
## System Requirements
@@ -49,6 +52,30 @@ Make sure to pull submodules before building:
git submodule update --init --recursive
```
### Building the OpenXR VR version on Windows
To build the VR-enabled version of this fork, OpenXR support must be enabled and
the OpenXR loader binaries must be compiled before building Dolphin itself.
If you are generating a Visual Studio build with CMake, configure with
`-DENABLE_VR=ON`:
```sh
cmake -S . -B Build-vs2022 -G "Visual Studio 17 2022" -A x64 -DENABLE_VR=ON
```
Then build the OpenXR loader first:
```sh
cmake --build Build-vs2022 --config Release --target openxr_loader
```
Finally, build Dolphin:
```sh
cmake --build Build-vs2022 --config Release --target dolphin-emu
```
If you prefer building OpenXR separately first, the repository also includes
`Externals/OpenXR/build-win64/OPENXR.sln`.
The "Release" solution configuration includes performance optimizations for the best user experience but complicates debugging Dolphin.
The "Debug" solution configuration is significantly slower, more verbose and less permissive but makes debugging Dolphin easier.
+1
View File
@@ -50,6 +50,7 @@
#define TRI_USER_DIR "Triforce"
#define CONFIG_DIR "Config"
#define GAMESETTINGS_DIR "GameSettings"
#define GAMESETTINGSVR_DIR "GameSettingsVR"
#define MAPS_DIR "Maps"
#define CACHE_DIR "Cache"
#define COVERCACHE_DIR "GameCovers"
+2
View File
@@ -841,6 +841,8 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[D_TRIUSER_IDX] = s_user_paths[D_USER_IDX] + TRI_USER_DIR DIR_SEP;
s_user_paths[D_CONFIG_IDX] = s_user_paths[D_USER_IDX] + CONFIG_DIR DIR_SEP;
s_user_paths[D_GAMESETTINGS_IDX] = s_user_paths[D_USER_IDX] + GAMESETTINGS_DIR DIR_SEP;
s_user_paths[D_GAMESETTINGSVR_IDX] =
s_user_paths[D_USER_IDX] + GAMESETTINGSVR_DIR DIR_SEP;
s_user_paths[D_MAPS_IDX] = s_user_paths[D_USER_IDX] + MAPS_DIR DIR_SEP;
s_user_paths[D_CACHE_IDX] = s_user_paths[D_USER_IDX] + CACHE_DIR DIR_SEP;
s_user_paths[D_COVERCACHE_IDX] = s_user_paths[D_CACHE_IDX] + COVERCACHE_DIR DIR_SEP;
+1
View File
@@ -32,6 +32,7 @@ enum
D_CONFIG_IDX, // global settings
D_GAMESETTINGS_IDX, // user-specified settings which override both the global and the default
// settings (per game)
D_GAMESETTINGSVR_IDX, // per-game VR settings/shader override INIs
D_SKYLANDERS_IDX,
D_MAPS_IDX,
+1
View File
@@ -66,6 +66,7 @@ enum class LogType : int
SP1,
SYMBOLS,
VIDEO,
OPENXR,
VIDEOINTERFACE,
WII_IPC,
WIIMOTE,
@@ -150,6 +150,7 @@ LogManager::LogManager()
m_log[LogType::SP1] = {"SP1", "Serial Port 1"};
m_log[LogType::SYMBOLS] = {"SYMBOLS", "Symbols"};
m_log[LogType::VIDEO] = {"Video", "Video Backend"};
m_log[LogType::OPENXR] = {"OpenXR", "OpenXR"};
m_log[LogType::VIDEOINTERFACE] = {"VI", "Video Interface"};
m_log[LogType::WIIMOTE] = {"Wiimote", "Wii Remote"};
m_log[LogType::WII_IPC] = {"WII_IPC", "WII IPC"};
@@ -54,6 +54,9 @@ const Info<bool> GFX_LOG_RENDER_TIME_TO_FILE{{System::GFX, "Settings", "LogRende
const Info<bool> GFX_OVERLAY_STATS{{System::GFX, "Settings", "OverlayStats"}, false};
const Info<bool> GFX_OVERLAY_PROJ_STATS{{System::GFX, "Settings", "OverlayProjStats"}, false};
const Info<bool> GFX_OVERLAY_SCISSOR_STATS{{System::GFX, "Settings", "OverlayScissorStats"}, false};
const Info<bool> GFX_OVERLAY_SHADER_FLAGS{{System::GFX, "Settings", "OverlayShaderFlags"}, false};
const Info<bool> GFX_OVERLAY_SHADER_HUNTING{{System::GFX, "Settings", "OverlayShaderHunting"},
false};
const Info<bool> GFX_DUMP_TEXTURES{{System::GFX, "Settings", "DumpTextures"}, false};
const Info<bool> GFX_DUMP_MIP_TEXTURES{{System::GFX, "Settings", "DumpMipTextures"}, true};
const Info<bool> GFX_DUMP_BASE_TEXTURES{{System::GFX, "Settings", "DumpBaseTextures"}, true};
@@ -180,6 +183,35 @@ const Info<bool> GFX_STEREO_EFB_MONO_DEPTH{{System::GFX, "Stereoscopy", "StereoE
const Info<float> GFX_STEREO_DEPTH_PERCENTAGE{{System::GFX, "Stereoscopy", "StereoDepthPercentage"},
100};
// Graphics.VR
const Info<bool> GFX_VR_ENABLE_OPENXR{{System::GFX, "VR", "EnableOpenXR"}, false};
const Info<bool> GFX_VR_AUTO_IMMEDIATE_XFB{{System::GFX, "VR", "AutoImmediateXFB"}, true};
const Info<float> GFX_VR_UNITS_PER_METER{{System::GFX, "VR", "UnitsPerMeter"}, 1.0f};
const Info<float> GFX_VR_LEAN_BACK_ANGLE{{System::GFX, "VR", "LeanBackAngle"}, 0.0f};
const Info<float> GFX_VR_CAMERA_FORWARD{{System::GFX, "VR", "CameraForward"}, 0.0f};
const Info<bool> GFX_VR_VIRTUAL_SCREEN{{System::GFX, "VR", "VirtualScreen"}, true};
const Info<float> GFX_VR_SCREEN_DISTANCE{{System::GFX, "VR", "ScreenDistance"}, 1.5f};
const Info<float> GFX_VR_SCREEN_SIZE{{System::GFX, "VR", "ScreenSize"}, 1.5f};
const Info<float> GFX_VR_HEAD_LOCKED_CURVATURE{{System::GFX, "VR", "HeadLockedCurvature"}, 0.0f};
const Info<bool> GFX_VR_DONT_CLEAR_SCREEN{{System::GFX, "VR", "DontClearScreen"}, false};
const Info<bool> GFX_VR_LOAD_CUSTOM_SHADERS{{System::GFX, "VR", "LoadCustomShaders"}, false};
const Info<bool> GFX_VR_ENABLE_OPENXR_CONFIG_SCENE{{System::GFX, "VR", "EnableOpenXRConfigScene"},
true};
const Info<bool> GFX_VR_DISABLE_CPU_CULL{{System::GFX, "VR", "DisableCPUCull"}, false};
const Info<OpenXROpcodeReplayMode> GFX_VR_OPCODE_REPLAY{
{System::GFX, "VR", "OpcodeReplay"}, OpenXROpcodeReplayMode::Off};
const Info<bool> GFX_VR_AUTO_VBI_FROM_HMD{{System::GFX, "VR", "AutoVBIFromHMD"}, false};
const Info<bool> GFX_VR_AUTO_LAYER_SPREAD{{System::GFX, "VR", "AutoLayerSpread"}, true};
const Info<float> GFX_VR_LAYER_OFFSET{{System::GFX, "VR", "LayerOffset"}, 0.002f};
const Info<float> GFX_VR_ELEMENT_DEPTH{{System::GFX, "VR", "ElementDepth"}, 0.001f};
const Info<bool> GFX_VR_REMOVE_BARS{{System::GFX, "VR", "RemoveCinematicBars"}, true};
const Info<bool> GFX_VR_LOCK_HEAD_POSE{{System::GFX, "VR", "LockHeadPosePerFrame"}, true};
const Info<bool> GFX_VR_AR_MODE{{System::GFX, "VR", "ARMode"}, false};
const Info<bool> GFX_VR_AR_MODE_DEBUG{{System::GFX, "VR", "ARModeDebug"}, false};
const Info<float> GFX_VR_AR_BACKGROUND_ALPHA{{System::GFX, "VR", "ARBackgroundAlpha"}, 0.0f};
const Info<float> GFX_VR_GAMMA{{System::GFX, "VR", "Gamma"}, 1.0f};
const Info<int> GFX_VR_CLEAR_EFB_COPIES{{System::GFX, "VR", "ClearEFBCopies"}, 0};
// Graphics.Hacks
const Info<bool> GFX_HACK_EFB_ACCESS_ENABLE{{System::GFX, "Hacks", "EFBAccessEnable"}, false};
@@ -18,6 +18,7 @@ enum class ColorCorrectionRegion : int;
enum class TriState : int;
enum class FrameDumpResolutionType : int;
enum class VertexLoaderType : int;
enum class OpenXROpcodeReplayMode : int;
namespace Config
{
@@ -56,6 +57,8 @@ extern const Info<bool> GFX_LOG_RENDER_TIME_TO_FILE;
extern const Info<bool> GFX_OVERLAY_STATS;
extern const Info<bool> GFX_OVERLAY_PROJ_STATS;
extern const Info<bool> GFX_OVERLAY_SCISSOR_STATS;
extern const Info<bool> GFX_OVERLAY_SHADER_FLAGS;
extern const Info<bool> GFX_OVERLAY_SHADER_HUNTING;
extern const Info<bool> GFX_DUMP_TEXTURES;
extern const Info<bool> GFX_DUMP_MIP_TEXTURES;
extern const Info<bool> GFX_DUMP_BASE_TEXTURES;
@@ -156,6 +159,70 @@ extern const Info<float> GFX_STEREO_DEPTH_PERCENTAGE;
static constexpr float GFX_STEREO_DEPTH_MAXIMUM = 100;
static constexpr float GFX_STEREO_CONVERGENCE_MAXIMUM = 200;
// Graphics.VR
extern const Info<bool> GFX_VR_ENABLE_OPENXR;
extern const Info<bool> GFX_VR_AUTO_IMMEDIATE_XFB;
extern const Info<float> GFX_VR_UNITS_PER_METER;
extern const Info<float> GFX_VR_LEAN_BACK_ANGLE;
extern const Info<float> GFX_VR_CAMERA_FORWARD;
static constexpr float GFX_VR_UNITS_PER_METER_MIN = 0.1f;
static constexpr float GFX_VR_UNITS_PER_METER_MAX = 500.0f;
static constexpr float GFX_VR_UNITS_PER_METER_STEP = 0.1f;
static constexpr float GFX_VR_LEAN_BACK_ANGLE_MIN = -45.0f;
static constexpr float GFX_VR_LEAN_BACK_ANGLE_MAX = 45.0f;
static constexpr float GFX_VR_LEAN_BACK_ANGLE_STEP = 0.1f;
static constexpr float GFX_VR_CAMERA_FORWARD_MIN = -20.0f;
static constexpr float GFX_VR_CAMERA_FORWARD_MAX = 20.0f;
static constexpr float GFX_VR_CAMERA_FORWARD_STEP = 0.1f;
extern const Info<bool> GFX_VR_VIRTUAL_SCREEN;
extern const Info<float> GFX_VR_SCREEN_DISTANCE;
extern const Info<float> GFX_VR_SCREEN_SIZE;
extern const Info<float> GFX_VR_HEAD_LOCKED_CURVATURE;
extern const Info<bool> GFX_VR_DONT_CLEAR_SCREEN;
extern const Info<bool> GFX_VR_LOAD_CUSTOM_SHADERS;
extern const Info<bool> GFX_VR_ENABLE_OPENXR_CONFIG_SCENE;
extern const Info<bool> GFX_VR_DISABLE_CPU_CULL;
extern const Info<OpenXROpcodeReplayMode> GFX_VR_OPCODE_REPLAY;
extern const Info<bool> GFX_VR_AUTO_VBI_FROM_HMD;
extern const Info<bool> GFX_VR_AUTO_LAYER_SPREAD;
extern const Info<float> GFX_VR_LAYER_OFFSET;
extern const Info<float> GFX_VR_ELEMENT_DEPTH;
extern const Info<bool> GFX_VR_REMOVE_BARS;
extern const Info<bool> GFX_VR_LOCK_HEAD_POSE;
extern const Info<bool> GFX_VR_AR_MODE;
extern const Info<bool> GFX_VR_AR_MODE_DEBUG;
extern const Info<float> GFX_VR_AR_BACKGROUND_ALPHA;
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_MIN = 0.0f;
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_MAX = 1.0f;
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_STEP = 0.05f;
extern const Info<float> GFX_VR_GAMMA;
extern const Info<int> GFX_VR_CLEAR_EFB_COPIES;
static constexpr int GFX_VR_CLEAR_EFB_MIN = 0;
static constexpr int GFX_VR_CLEAR_EFB_MAX = 640;
static constexpr int GFX_VR_CLEAR_EFB_STEP = 10;
static constexpr float GFX_VR_SCREEN_DISTANCE_MIN = 0.5f;
static constexpr float GFX_VR_SCREEN_DISTANCE_MAX = 10.0f;
static constexpr float GFX_VR_SCREEN_DISTANCE_STEP = 0.1f;
static constexpr float GFX_VR_SCREEN_SIZE_MIN = 0.5f;
static constexpr float GFX_VR_SCREEN_SIZE_MAX = 5.0f;
static constexpr float GFX_VR_SCREEN_SIZE_STEP = 0.1f;
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_MIN = 0.0f;
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_MAX = 5.0f;
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_STEP = 0.01f;
static constexpr float GFX_VR_LAYER_OFFSET_MIN = 0.0001f;
static constexpr float GFX_VR_LAYER_OFFSET_MAX = 0.01f;
static constexpr float GFX_VR_LAYER_OFFSET_STEP = 0.0001f;
static constexpr float GFX_VR_ELEMENT_DEPTH_MIN = 0.0f;
static constexpr float GFX_VR_ELEMENT_DEPTH_MAX = 0.1f;
static constexpr float GFX_VR_ELEMENT_DEPTH_STEP = 0.0001f;
static constexpr float GFX_VR_GAMMA_MIN = 1.0f;
static constexpr float GFX_VR_GAMMA_MAX = 3.0f;
static constexpr float GFX_VR_GAMMA_STEP = 0.1f;
// Graphics.Hacks
extern const Info<bool> GFX_HACK_EFB_ACCESS_ENABLE;
+128 -2
View File
@@ -4,6 +4,8 @@
#include "Core/ConfigManager.h"
#include <algorithm>
#include <fstream>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
@@ -11,8 +13,6 @@
#include <string_view>
#include <variant>
#include <Core/Core.h>
#include <fmt/format.h>
#include "AudioCommon/AudioCommon.h"
@@ -29,6 +29,7 @@
#include "Core/AchievementManager.h"
#include "Core/Boot/Boot.h"
#include "Core/Config/DefaultLocale.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/SYSCONFSettings.h"
#include "Core/ConfigLoaders/GameConfigLoader.h"
@@ -58,6 +59,124 @@
#include "DiscIO/Volume.h"
#include "DiscIO/VolumeWad.h"
namespace
{
constexpr std::string_view VR_SECTION_NAME = "Graphics.VR";
constexpr std::string_view LEGACY_VR_SECTION_NAME = "GFX.VR";
using VRSettingMap = std::map<std::string, std::string, Common::CaseInsensitiveLess>;
static std::string GetVRGameINIPath(std::string_view game_id)
{
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + std::string(game_id) + ".ini";
}
static bool IsVRSectionHeader(std::string_view line)
{
if (line.size() < 3 || line.front() != '[' || line.back() != ']')
return false;
const std::string_view section_name = line.substr(1, line.size() - 2);
return Common::CaseInsensitiveEquals(section_name, VR_SECTION_NAME) ||
Common::CaseInsensitiveEquals(section_name, LEGACY_VR_SECTION_NAME);
}
static VRSettingMap LoadVRSettingsFromINI(std::string_view game_id)
{
VRSettingMap values;
if (game_id.empty() || game_id == DEFAULT_GAME_ID)
return values;
std::ifstream file(GetVRGameINIPath(game_id));
if (!file.is_open())
return values;
bool in_vr_section = false;
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
const std::string_view trimmed = StripWhitespace(line);
if (trimmed.empty())
continue;
if (!trimmed.empty() && trimmed.front() == '[')
{
if (IsVRSectionHeader(trimmed))
{
in_vr_section = true;
continue;
}
if (in_vr_section)
break;
}
if (!in_vr_section)
continue;
if (trimmed.front() == '#' || trimmed.front() == ';' || trimmed.front() == '$' ||
trimmed.front() == '*')
{
continue;
}
std::string key;
std::string value;
Common::IniFile::ParseLine(trimmed, &key, &value);
if (!key.empty())
values.insert_or_assign(std::move(key), std::move(value));
}
return values;
}
template <typename T>
static void ApplyVRSetting(const VRSettingMap& values, const char* key, const Config::Info<T>& info)
{
const auto it = values.find(key);
if (it == values.end())
{
Config::DeleteKey(Config::LayerType::CurrentRun, info);
return;
}
T parsed_value{};
if (TryParse(it->second, &parsed_value))
Config::SetCurrent(info, parsed_value);
else
Config::DeleteKey(Config::LayerType::CurrentRun, info);
}
static void ApplyGameVRConfigOverrides(std::string_view game_id)
{
const Config::ConfigChangeCallbackGuard guard;
const VRSettingMap values = LoadVRSettingsFromINI(game_id);
ApplyVRSetting(values, "EnableOpenXR", Config::GFX_VR_ENABLE_OPENXR);
ApplyVRSetting(values, "UnitsPerMeter", Config::GFX_VR_UNITS_PER_METER);
ApplyVRSetting(values, "LeanBackAngle", Config::GFX_VR_LEAN_BACK_ANGLE);
ApplyVRSetting(values, "CameraForward", Config::GFX_VR_CAMERA_FORWARD);
ApplyVRSetting(values, "VirtualScreen", Config::GFX_VR_VIRTUAL_SCREEN);
ApplyVRSetting(values, "ScreenDistance", Config::GFX_VR_SCREEN_DISTANCE);
ApplyVRSetting(values, "ScreenSize", Config::GFX_VR_SCREEN_SIZE);
ApplyVRSetting(values, "DontClearScreen", Config::GFX_VR_DONT_CLEAR_SCREEN);
ApplyVRSetting(values, "LoadCustomShaders", Config::GFX_VR_LOAD_CUSTOM_SHADERS);
ApplyVRSetting(values, "DisableCPUCull", Config::GFX_VR_DISABLE_CPU_CULL);
ApplyVRSetting(values, "AutoVBIFromHMD", Config::GFX_VR_AUTO_VBI_FROM_HMD);
ApplyVRSetting(values, "AutoLayerSpread", Config::GFX_VR_AUTO_LAYER_SPREAD);
ApplyVRSetting(values, "LayerOffset", Config::GFX_VR_LAYER_OFFSET);
ApplyVRSetting(values, "ElementDepth", Config::GFX_VR_ELEMENT_DEPTH);
ApplyVRSetting(values, "ClearEFBCopies", Config::GFX_VR_CLEAR_EFB_COPIES);
ApplyVRSetting(values, "ARMode", Config::GFX_VR_AR_MODE);
ApplyVRSetting(values, "ARModeDebug", Config::GFX_VR_AR_MODE_DEBUG);
ApplyVRSetting(values, "ARBackgroundAlpha", Config::GFX_VR_AR_BACKGROUND_ALPHA);
}
} // namespace
SConfig* SConfig::m_Instance;
SConfig::SConfig()
@@ -253,11 +372,18 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri
Config::AddLayer(ConfigLoaders::GenerateGlobalGameConfigLoader(game_id, revision));
Config::AddLayer(ConfigLoaders::GenerateLocalGameConfigLoader(game_id, revision));
ApplyGameVRConfigOverrides(game_id);
if (is_running_or_starting)
DolphinAnalytics::Instance().ReportGameStart();
}
void SConfig::ReloadGameVRConfigOverrides()
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
ApplyGameVRConfigOverrides(m_game_id);
}
void SConfig::OnESTitleChanged()
{
auto& system = Core::System::GetInstance();
+1
View File
@@ -89,6 +89,7 @@ struct SConfig
Common::IniFile LoadDefaultGameIni() const;
Common::IniFile LoadLocalGameIni() const;
Common::IniFile LoadGameIni() const;
void ReloadGameVRConfigOverrides();
static Common::IniFile LoadDefaultGameIni(std::string_view id, std::optional<u16> revision);
static Common::IniFile LoadLocalGameIni(std::string_view id, std::optional<u16> revision);
+20 -4
View File
@@ -19,6 +19,7 @@
#include "Core/AchievementManager.h"
#include "Core/CPUThreadConfigCallback.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/HW/SystemTimers.h"
@@ -35,6 +36,24 @@
namespace CoreTiming
{
static constexpr int MAX_SLICE_LENGTH = 20000;
static constexpr float GC_BASE_VBI_FREQUENCY_HZ = 59.94f;
static constexpr float VR_FORCED_VBI_FREQUENCY_HZ = 90.0f;
static float GetEffectiveVIOverclockFactor()
{
float vi_factor = Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
1.0f;
if (Config::Get(Config::GFX_VR_AUTO_VBI_FROM_HMD) && Config::Get(Config::GFX_VR_ENABLE_OPENXR))
vi_factor = VR_FORCED_VBI_FREQUENCY_HZ / GC_BASE_VBI_FREQUENCY_HZ;
return std::max(vi_factor, 0.01f);
}
static float GetEffectiveTotalOverclockFactor()
{
const float cpu_factor =
Config::Get(Config::MAIN_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_OVERCLOCK) : 1.0f;
return cpu_factor * GetEffectiveVIOverclockFactor();
}
static void EmptyTimedCallback(Core::System& system, u64 userdata, s64 cyclesLate)
{
@@ -136,10 +155,7 @@ void CoreTimingManager::Shutdown()
void CoreTimingManager::RefreshConfig()
{
m_config_oc_factor =
(Config::Get(Config::MAIN_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_OVERCLOCK) : 1.0f) *
(Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
1.0f);
m_config_oc_factor = GetEffectiveTotalOverclockFactor();
m_config_oc_inv_factor = 1.0f / m_config_oc_factor;
m_config_sync_on_skip_idle = Config::Get(Config::MAIN_SYNC_ON_SKIP_IDLE);
m_config_rush_frame_presentation = Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION);
+12 -2
View File
@@ -54,6 +54,17 @@ static constexpr std::array<u32, 2> CLOCK_FREQUENCIES{{
}};
static constexpr u32 NUM_HALF_LINES_FOR_SI_POLL = (7 * 2) + 1; // this is how long an SI poll takes
static constexpr float GC_BASE_VBI_FREQUENCY_HZ = 59.94f;
static constexpr float VR_FORCED_VBI_FREQUENCY_HZ = 90.0f;
static float GetEffectiveVBIOverrideFactor()
{
float factor = Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
1.0f;
if (Config::Get(Config::GFX_VR_AUTO_VBI_FROM_HMD) && Config::Get(Config::GFX_VR_ENABLE_OPENXR))
factor = VR_FORCED_VBI_FREQUENCY_HZ / GC_BASE_VBI_FREQUENCY_HZ;
return std::max(factor, 0.01f);
}
void VideoInterfaceManager::DoState(PointerWrap& p)
{
@@ -187,8 +198,7 @@ void VideoInterfaceManager::Init()
void VideoInterfaceManager::RefreshConfig()
{
m_config_vi_oc_factor =
Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) : 1.0f;
m_config_vi_oc_factor = GetEffectiveVBIOverrideFactor();
if (AchievementManager::GetInstance().IsHardcoreModeActive() && m_config_vi_oc_factor < 1.0f)
{
Config::SetCurrent(Config::MAIN_VI_OVERCLOCK, 1.0f);
+465 -1
View File
@@ -3,16 +3,26 @@
#include "Core/HW/Wiimote.h"
#include <chrono>
#include <cmath>
#include <limits>
#include <optional>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/Logging/Log.h"
#include "Common/MathUtil.h"
#include "Common/Matrix.h"
#ifdef ENABLE_VR
#include "Common/VR/OpenXRInputState.h"
#endif
#include "Core/Config/WiimoteSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "Core/HW/WiimoteEmu/Camera.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
@@ -22,6 +32,9 @@
#include "Core/WiiUtils.h"
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
#include "InputCommon/ControllerEmu/StickGate.h"
#include "InputCommon/InputConfig.h"
// Limit the amount of wiimote connect requests, when a button is pressed in disconnected state
@@ -32,6 +45,401 @@ namespace
static std::array<std::atomic<WiimoteSource>, MAX_BBMOTES> s_wiimote_sources;
static std::optional<Config::ConfigChangedCallbackID> s_config_callback_id = std::nullopt;
bool IsEmulatedSource(WiimoteSource source)
{
return source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR;
}
#ifdef ENABLE_VR
Common::Quaternion ToQuaternion(const std::array<float, 4>& quat)
{
return {quat[3], quat[0], quat[1], quat[2]};
}
Common::Vec3 ToVec3(const std::array<float, 3>& vec)
{
return {vec[0], vec[1], vec[2]};
}
struct OpenXRVelocityHistory
{
bool has_pose_sample = false;
bool has_velocity_sample = false;
Common::Vec3 previous_position{};
Common::Vec3 previous_velocity{};
std::chrono::steady_clock::time_point previous_time{};
};
struct OpenXRWiimoteState
{
u64 generation = std::numeric_limits<u64>::max();
Common::Vec3 acceleration{0.0f, 0.0f, float(MathUtil::GRAVITY_ACCELERATION)};
Common::Vec3 angular_velocity{};
float ir_x = std::numeric_limits<float>::quiet_NaN();
float ir_y = 0.0f;
float ir_z = 0.0f; // Forward/backward distance offset (-1 to +1)
// Raw aim orientation for reference-based IR computation in ApplyOpenXRIRCenter
Common::Quaternion aim_orientation{1.0f, 0.0f, 0.0f, 0.0f};
Common::Vec3 aim_position{}; // Aim pose world position for Z computation
bool has_aim = false;
};
struct OpenXRIRCenter
{
bool initialized = false;
// Reference orientation from the HMD — "where the user is looking" becomes cursor center.
// Controller aim deviation from this direction maps to cursor position.
Common::Quaternion reference_orientation;
Common::Vec3 reference_position{}; // Controller reference position for forward/backward
};
struct OpenXRCursorRanges
{
float yaw_half_range = float(MathUtil::PI * 25.0 / 180.0 / 2.0);
float pitch_half_range = float(MathUtil::PI * 20.0 / 180.0 / 2.0);
};
OpenXRCursorRanges GetOpenXRCursorRanges(unsigned int wiimote_index)
{
OpenXRCursorRanges ranges;
if (wiimote_index >= MAX_WIIMOTES)
return ranges;
auto* config = ::Wiimote::GetConfig();
if (!config)
return ranges;
auto* wiimote = static_cast<WiimoteEmu::Wiimote*>(config->GetController(wiimote_index));
if (!wiimote)
return ranges;
auto* ir_group = static_cast<ControllerEmu::Cursor*>(
wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Point));
if (!ir_group)
return ranges;
ranges.yaw_half_range = std::max(static_cast<float>(ir_group->GetTotalYaw() / 2), 0.001f);
ranges.pitch_half_range = std::max(static_cast<float>(ir_group->GetTotalPitch() / 2), 0.001f);
return ranges;
}
OpenXRWiimoteState BuildOpenXRState(const Common::VR::OpenXRControllerState& controller,
OpenXRVelocityHistory* velocity_history)
{
OpenXRWiimoteState out;
const bool has_grip_pose = controller.grip_pose.valid;
const bool has_aim_pose = controller.aim_pose.valid;
if (!has_grip_pose && !has_aim_pose)
return out;
// Use aim pose for the motion reference frame. The aim pose orientation determines how
// world-space gravity maps to Wiimote-local accelerometer axes. Using grip would produce
// wrong accel values because grip/aim can differ by 30-60 degrees on many controllers.
const Common::Quaternion reference_orientation =
(has_aim_pose ? ToQuaternion(controller.aim_pose.orientation) :
ToQuaternion(controller.grip_pose.orientation))
.Normalized();
const Common::Matrix33 world_to_local =
Common::Matrix33::FromQuaternion(reference_orientation).Transposed();
const auto get_matrix = [&world_to_local](int row, int col) { return world_to_local.data[row * 3 + col]; };
Common::Vec3 relative_acceleration{};
const auto now = std::chrono::steady_clock::now();
const float dt =
velocity_history->has_pose_sample
? std::chrono::duration_cast<std::chrono::duration<float>>(now -
velocity_history->previous_time)
.count()
: 0.0f;
Common::Vec3 current_position{};
std::optional<Common::Vec3> pose_velocity;
if (has_grip_pose)
{
current_position = ToVec3(controller.grip_pose.position);
if (velocity_history->has_pose_sample && dt > 0.001f)
pose_velocity = (current_position - velocity_history->previous_position) / dt;
}
std::optional<Common::Vec3> current_velocity;
if (controller.grip_velocity.linear_valid)
current_velocity = ToVec3(controller.grip_velocity.linear);
if (pose_velocity)
{
// Blend runtime velocity with pose-derived velocity to improve push/pull response on
// runtimes that heavily smooth linear velocity.
if (current_velocity)
current_velocity = (*current_velocity + *pose_velocity) * 0.5f;
else
current_velocity = *pose_velocity;
}
if (current_velocity && velocity_history->has_velocity_sample && dt > 0.001f)
{
const Common::Vec3 world_accel = (*current_velocity - velocity_history->previous_velocity) / dt;
relative_acceleration = world_to_local * world_accel;
}
if (has_grip_pose)
{
velocity_history->previous_position = current_position;
velocity_history->previous_time = now;
velocity_history->has_pose_sample = true;
}
else
{
velocity_history->has_pose_sample = false;
velocity_history->has_velocity_sample = false;
}
if (current_velocity)
{
velocity_history->previous_velocity = *current_velocity;
velocity_history->has_velocity_sample = true;
}
else if (!velocity_history->has_pose_sample)
{
velocity_history->has_velocity_sample = false;
}
float gx = -get_matrix(0, 1);
float gz = get_matrix(1, 1);
float gy = get_matrix(2, 1);
gx -= relative_acceleration.x / float(MathUtil::GRAVITY_ACCELERATION);
gz += relative_acceleration.y / float(MathUtil::GRAVITY_ACCELERATION);
gy += relative_acceleration.z / float(MathUtil::GRAVITY_ACCELERATION);
out.acceleration = Common::Vec3(gx, gy, gz) * float(MathUtil::GRAVITY_ACCELERATION);
if (controller.grip_velocity.angular_valid)
{
const Common::Vec3 world_angular_velocity = ToVec3(controller.grip_velocity.angular);
const Common::Vec3 local_angular_velocity = world_to_local * world_angular_velocity;
out.angular_velocity =
Common::Vec3(local_angular_velocity.x, local_angular_velocity.z, local_angular_velocity.y);
}
// Store raw aim orientation and position — IR angles and distance are computed
// in ApplyOpenXRIRCenter relative to the captured reference, making the mapping
// headset-independent.
if (has_aim_pose)
{
out.aim_orientation = ToQuaternion(controller.aim_pose.orientation).Normalized();
out.aim_position = ToVec3(controller.aim_pose.position);
out.has_aim = true;
}
else
{
out.aim_orientation = reference_orientation;
out.has_aim = false;
}
// IR x/y/z left as NaN/0/0 — will be filled by ApplyOpenXRIRCenter
return out;
}
std::array<OpenXRIRCenter, MAX_BBMOTES> s_openxr_ir_centers{};
void ApplyOpenXRIRCenter(unsigned int wiimote_index, OpenXRWiimoteState* state,
const Common::VR::OpenXRPoseState& head_pose)
{
if (wiimote_index >= MAX_BBMOTES || !state->has_aim)
return;
auto& center = s_openxr_ir_centers[wiimote_index];
// On first valid frame or after recenter, capture the HMD forward direction as reference.
// The cursor center corresponds to where the user is looking, and controller aim
// deviation from that direction moves the cursor.
if (!center.initialized)
{
if (head_pose.valid)
center.reference_orientation = ToQuaternion(head_pose.orientation).Normalized();
else
center.reference_orientation = state->aim_orientation;
center.reference_position = state->aim_position;
center.initialized = true;
}
// Compute the relative rotation: how much the controller aim deviates from the
// HMD forward direction. conjugate(reference) * current_aim
const Common::Quaternion relative =
center.reference_orientation.Conjugate() * state->aim_orientation;
// Transform the forward vector (-Z) by the relative rotation to get the
// local-frame pointing direction
const Common::Vec3 local_forward = relative * Common::Vec3{0.0f, 0.0f, -1.0f};
// Extract yaw and pitch from the local forward vector
// In OpenXR space: X=right, Y=up, Z=back (right-handed)
// local_forward.x = left/right deviation (yaw)
// local_forward.y = up/down deviation (pitch)
// local_forward.z = forward component (should be near -1 when pointing straight)
const float forward_len =
std::sqrt(local_forward.x * local_forward.x + local_forward.z * local_forward.z);
const float yaw = std::atan2(local_forward.x, -local_forward.z);
const float pitch = std::atan2(local_forward.y, std::max(forward_len, 0.001f));
// Get per-wiimote cursor ranges for normalization
const OpenXRCursorRanges ranges = GetOpenXRCursorRanges(wiimote_index);
// Normalize to [-1, 1] range based on configured total yaw/pitch
// Positive yaw = pointing right = positive ir_x (moves cursor right)
// Positive pitch = pointing up = positive ir_y (moves cursor up)
state->ir_x = yaw / std::max(ranges.yaw_half_range, 0.001f);
state->ir_y = pitch / std::max(ranges.pitch_half_range, 0.001f);
// Compute forward/backward distance (ir_z) from position delta projected onto
// the reference aim direction. This changes the spacing between the two virtual
// IR sensor bar dots — closer = dots farther apart, farther = dots closer together.
// Scale: 0.3m of forward movement = ir_z of -1 (moves 1m closer to sensor bar).
// Matches the old Hydra behavior (300mm per unit for Z axis).
constexpr float Z_METERS_PER_UNIT = 0.3f;
const Common::Vec3 ref_forward =
center.reference_orientation * Common::Vec3{0.0f, 0.0f, -1.0f};
const Common::Vec3 pos_delta = state->aim_position - center.reference_position;
const float forward_displacement = pos_delta.Dot(ref_forward);
state->ir_z = std::clamp(-forward_displacement / Z_METERS_PER_UNIT, -1.0f, 1.0f);
}
ControllerEmu::InputOverrideFunction CreateOpenXRInputOverrideFunction(unsigned int wiimote_index,
bool prefer_left_hand)
{
OpenXRWiimoteState cached_state;
OpenXRVelocityHistory left_velocity_history;
OpenXRVelocityHistory right_velocity_history;
return [wiimote_index, prefer_left_hand, cached_state, left_velocity_history,
right_velocity_history](std::string_view group_name, std::string_view control_name,
ControlState) mutable -> std::optional<ControlState> {
if (s_wiimote_sources[wiimote_index].load() != WiimoteSource::OpenXR)
return std::nullopt;
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
if (!snapshot.runtime_active)
return std::nullopt;
if (cached_state.generation != snapshot.generation)
{
const auto& left = snapshot.controllers[0];
const auto& right = snapshot.controllers[1];
const auto right_valid = right.grip_pose.valid || right.aim_pose.valid;
const auto left_valid = left.grip_pose.valid || left.aim_pose.valid;
if (!prefer_left_hand && right_valid)
{
cached_state =
BuildOpenXRState(right, &right_velocity_history);
ApplyOpenXRIRCenter(wiimote_index, &cached_state, snapshot.head_pose);
}
else if (left_valid)
{
cached_state =
BuildOpenXRState(left, &left_velocity_history);
ApplyOpenXRIRCenter(wiimote_index, &cached_state, snapshot.head_pose);
}
else
{
cached_state = {};
}
cached_state.generation = snapshot.generation;
}
if (group_name == WiimoteEmu::Wiimote::ACCELEROMETER_GROUP)
{
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
return cached_state.acceleration.x;
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
return cached_state.acceleration.y;
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
return cached_state.acceleration.z;
}
else if (group_name == WiimoteEmu::Wiimote::GYROSCOPE_GROUP)
{
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
return cached_state.angular_velocity.x;
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
return cached_state.angular_velocity.y;
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
return cached_state.angular_velocity.z;
}
else if (group_name == WiimoteEmu::Wiimote::IR_GROUP)
{
// Handle recenter signal from EmulatePoint — reset the reference orientation
// so the current aim direction becomes the new center.
if (control_name == "Recenter")
{
if (wiimote_index < MAX_BBMOTES)
s_openxr_ir_centers[wiimote_index].initialized = false;
return std::nullopt;
}
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
return std::isnan(cached_state.ir_x) ? 0.0 : cached_state.ir_x;
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
return static_cast<double>(cached_state.ir_y);
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
return static_cast<double>(cached_state.ir_z);
}
return std::nullopt;
};
}
std::array<bool, MAX_BBMOTES> s_openxr_overrides_enabled{};
void UpdateOpenXRInputOverride(unsigned int index, WiimoteSource source)
{
auto* wiimote =
static_cast<WiimoteEmu::Wiimote*>(::Wiimote::GetConfig()->GetController(index));
if (!wiimote)
return;
auto apply_to_attachments = [wiimote](const ControllerEmu::InputOverrideFunction& override_func) {
auto* attachments_group = static_cast<ControllerEmu::Attachments*>(
wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Attachments));
if (!attachments_group)
return;
for (const auto& attachment : attachments_group->GetAttachmentList())
{
if (!attachment)
continue;
if (override_func)
attachment->SetInputOverrideFunction(override_func);
else
attachment->ClearInputOverrideFunction();
}
};
if (source == WiimoteSource::OpenXR)
{
s_openxr_ir_centers[index] = {};
wiimote->SetInputOverrideFunction(CreateOpenXRInputOverrideFunction(index, false));
apply_to_attachments(CreateOpenXRInputOverrideFunction(index, true));
s_openxr_overrides_enabled[index] = true;
}
else if (s_openxr_overrides_enabled[index])
{
s_openxr_ir_centers[index] = {};
wiimote->ClearInputOverrideFunction();
apply_to_attachments({});
s_openxr_overrides_enabled[index] = false;
}
}
#else
void UpdateOpenXRInputOverride(unsigned int, WiimoteSource)
{
}
#endif
WiimoteSource GetSource(unsigned int index)
{
return s_wiimote_sources[index];
@@ -44,9 +452,12 @@ void OnSourceChanged(unsigned int index, WiimoteSource source)
if (previous_source == source)
{
// No change. Do nothing.
UpdateOpenXRInputOverride(index, source);
return;
}
UpdateOpenXRInputOverride(index, source);
WiimoteReal::HandleWiimoteSourceChange(index);
const Core::CPUThreadGuard guard(Core::System::GetInstance());
@@ -79,6 +490,7 @@ HIDWiimote* GetHIDWiimoteSource(unsigned int index)
switch (GetSource(index))
{
case WiimoteSource::Emulated:
case WiimoteSource::OpenXR:
hid_source = static_cast<WiimoteEmu::Wiimote*>(::Wiimote::GetConfig()->GetController(index));
break;
@@ -104,6 +516,49 @@ InputConfig* GetConfig()
return &s_config;
}
std::optional<OpenXRWiiRemoteState> GetOpenXRWiiRemoteState(unsigned int index)
{
#ifdef ENABLE_VR
if (index >= MAX_WIIMOTES || s_wiimote_sources[index].load() != WiimoteSource::OpenXR)
return std::nullopt;
if (const auto right = GetOpenXRHandState(false))
return right;
return GetOpenXRHandState(true);
#else
return std::nullopt;
#endif
}
std::optional<OpenXRWiiRemoteState> GetOpenXRHandState(bool left_hand)
{
#ifdef ENABLE_VR
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
if (!snapshot.runtime_active)
return std::nullopt;
const auto& controller = snapshot.controllers[left_hand ? 0 : 1];
if (!controller.grip_pose.valid && !controller.aim_pose.valid)
return std::nullopt;
OpenXRVelocityHistory throwaway_velocity_history;
OpenXRWiimoteState state =
BuildOpenXRState(controller, &throwaway_velocity_history);
// This path doesn't have a wiimote index, use 0 for reference-based IR
ApplyOpenXRIRCenter(0, &state, snapshot.head_pose);
OpenXRWiiRemoteState out;
out.acceleration = {state.acceleration.x, state.acceleration.y, state.acceleration.z};
out.angular_velocity = {state.angular_velocity.x, state.angular_velocity.y, state.angular_velocity.z};
out.ir_x = state.ir_x;
out.ir_y = state.ir_y;
out.ir_visible = !std::isnan(state.ir_x);
return out;
#else
return std::nullopt;
#endif
}
ControllerEmu::ControlGroup* GetWiimoteGroup(int number, WiimoteEmu::WiimoteGroup group)
{
return static_cast<WiimoteEmu::Wiimote*>(s_config.GetController(number))->GetWiimoteGroup(group);
@@ -233,7 +688,7 @@ void DoState(PointerWrap& p)
auto state_wiimote_source = u8(source);
p.Do(state_wiimote_source);
if (WiimoteSource(state_wiimote_source) == WiimoteSource::Emulated)
if (IsEmulatedSource(WiimoteSource(state_wiimote_source)))
{
// Sync complete state of emulated wiimotes.
static_cast<WiimoteEmu::Wiimote*>(s_config.GetController(i))->DoState(p);
@@ -248,4 +703,13 @@ void DoState(PointerWrap& p)
}
}
}
void RecenterOpenXRPointer(unsigned int wiimote_index)
{
#ifdef ENABLE_VR
if (wiimote_index < MAX_BBMOTES)
s_openxr_ir_centers[wiimote_index].initialized = false;
#endif
}
} // namespace Wiimote
+14
View File
@@ -5,6 +5,7 @@
#include <array>
#include <atomic>
#include <optional>
#include "Common/Common.h"
#include "Common/CommonTypes.h"
@@ -49,6 +50,7 @@ enum class WiimoteSource
None = 0,
Emulated = 1,
Real = 2,
OpenXR = 3,
};
namespace WiimoteCommon
@@ -74,6 +76,15 @@ enum class InitializeMode
// The Real Wii Remote sends report every ~5ms (200 Hz).
constexpr int UPDATE_FREQ = 200;
struct OpenXRWiiRemoteState
{
std::array<float, 3> acceleration{};
std::array<float, 3> angular_velocity{};
float ir_x = 0.0f;
float ir_y = 0.0f;
bool ir_visible = false;
};
void Shutdown();
void Initialize(InitializeMode init_mode);
void ResetAllWiimotes();
@@ -81,7 +92,10 @@ void LoadConfig();
void GenerateDynamicInputTextures();
void Resume();
void Pause();
std::optional<OpenXRWiiRemoteState> GetOpenXRWiiRemoteState(unsigned int index);
std::optional<OpenXRWiiRemoteState> GetOpenXRHandState(bool left_hand);
void RecenterOpenXRPointer(unsigned int wiimote_index);
void DoState(PointerWrap& p);
InputConfig* GetConfig();
ControllerEmu::ControlGroup* GetWiimoteGroup(int number, WiimoteEmu::WiimoteGroup group);
+50 -2
View File
@@ -228,8 +228,13 @@ void EmulatePoint(MotionState* state, ControllerEmu::Cursor* ir_group,
if (!cursor.IsVisible())
{
const float recenter_x = state->recenter_x;
const float recenter_y = state->recenter_y;
// Move the wiimote a kilometer forward so the sensor bar is always behind it.
*state = {};
state->recenter_x = recenter_x;
state->recenter_y = recenter_y;
state->position = {0, -1000, 0};
return;
}
@@ -246,12 +251,55 @@ void EmulatePoint(MotionState* state, ControllerEmu::Cursor* ir_group,
const float yaw_scale = ir_group->GetTotalYaw() / 2;
const float pitch_scale = ir_group->GetTotalPitch() / 2;
// Check for Z override (forward/backward distance from VR controllers).
// Z override is in [-1, 1] range: -1 = closest (1m), 0 = neutral (2m), +1 = farthest (3m).
float distance = NEUTRAL_DISTANCE;
if (override_func)
{
if (const auto z_override = override_func(ir_group->name,
ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE, 0.0))
{
distance = NEUTRAL_DISTANCE + static_cast<float>(*z_override);
distance = std::clamp(distance, 0.5f, 4.0f);
}
}
// Just jump to the target position.
state->position = {0, NEUTRAL_DISTANCE, -height};
state->position = {0, distance, -height};
state->velocity = {};
state->acceleration = {};
const auto target_angle = Common::Vec3(pitch_scale * -cursor.y, 0, yaw_scale * -cursor.x);
float centered_x, centered_y;
const bool recenter_pressed = ir_group->controls[5]->GetState<bool>();
if (override_func)
{
// When an input override is active (e.g. OpenXR VR controllers), recentering resets
// the override's reference orientation via a "Recenter" signal. Skip the offset-based
// recenter to avoid shifting the usable cursor range off-screen.
if (recenter_pressed && !state->recenter_button_held)
override_func(ir_group->name, "Recenter", 0.0);
state->recenter_button_held = recenter_pressed;
centered_x = std::clamp(cursor.x, -1.0, 1.0);
centered_y = std::clamp(cursor.y, -1.0, 1.0);
state->recenter_x = 0.0f;
state->recenter_y = 0.0f;
}
else
{
if (recenter_pressed && !state->recenter_button_held)
{
state->recenter_x = cursor.x;
state->recenter_y = cursor.y;
}
state->recenter_button_held = recenter_pressed;
centered_x = std::clamp(cursor.x - state->recenter_x, -1.0, 1.0);
centered_y = std::clamp(cursor.y - state->recenter_y, -1.0, 1.0);
}
const auto target_angle = Common::Vec3(pitch_scale * -centered_y, 0, yaw_scale * -centered_x);
// If cursor was hidden, jump to the target angle immediately.
if (state->position.y < 0)
@@ -47,6 +47,11 @@ struct IMUCursorState
struct MotionState : PositionalState, RotationalState
{
MotionState() = default;
// Cursor center offset used by pointer recenter.
float recenter_x = 0.0f;
float recenter_y = 0.0f;
bool recenter_button_held = false;
};
// Note that 'gyroscope' is rotation of world around device.
@@ -82,10 +82,12 @@ void Nunchuk::BuildDesiredExtensionState(DesiredExtensionState* target_state)
const auto transformation =
GetRotationalMatrix(-m_tilt_state.angle) * GetRotationalMatrix(-m_swing_state.angle);
Common::Vec3 imu_accel =
m_imu_accelerometer->GetState().value_or(Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)));
Common::Vec3 accel =
transformation *
(m_swing_state.acceleration +
m_imu_accelerometer->GetState().value_or(Common::Vec3(0, 0, float(GRAVITY_ACCELERATION))));
(m_swing_state.acceleration + imu_accel);
// shake
accel += m_shake_state.acceleration;
@@ -20,6 +20,7 @@
#include "Common/MathUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/SYSCONFSettings.h"
#include "Core/Core.h"
#include "Core/HW/Wiimote.h"
@@ -509,6 +510,7 @@ void Wiimote::BuildDesiredWiimoteState(DesiredWiimoteState* target_state,
GetTotalTransformation(),
Common::Vec2(m_fov_x_setting.GetValue(), m_fov_y_setting.GetValue()) / 360 *
float(MathUtil::TAU));
}
else
{
@@ -626,13 +628,16 @@ void Wiimote::SendDataReport(const DesiredWiimoteState& target_state)
u8* ir_data = rpt_builder.GetIRDataPtr();
const u8 ir_size = rpt_builder.GetIRDataSize();
const int ir_read_size =
m_i2c_bus.BusRead(CameraLogic::I2C_ADDR, camera_data_offset, ir_size, ir_data);
if (ir_size != m_i2c_bus.BusRead(CameraLogic::I2C_ADDR, camera_data_offset, ir_size, ir_data))
if (ir_size != ir_read_size)
{
// This happens when IR reporting is enabled but the camera hardware is disabled.
// It commonly occurs when changing IR sensitivity.
std::fill_n(ir_data, ir_size, u8(0xff));
}
}
// Extension port:
+32
View File
@@ -114,6 +114,36 @@ constexpr std::array<const char*, NUM_HOTKEYS> s_hotkey_labels{{
_trans("Toggle Texture Dumping"),
_trans("Toggle Custom Textures"),
_trans("Toggle OpenXR"),
_trans("Reset VR Position"),
_trans("Decrease Units Per Meter"),
_trans("Increase Units Per Meter"),
_trans("Decrease Lean Back Angle"),
_trans("Increase Lean Back Angle"),
_trans("Decrease Camera Forward"),
_trans("Increase Camera Forward"),
_trans("Toggle Virtual Screen"),
_trans("Decrease Screen Distance"),
_trans("Increase Screen Distance"),
_trans("Decrease Screen Size"),
_trans("Increase Screen Size"),
_trans("Decrease Screen Curvature"),
_trans("Increase Screen Curvature"),
_trans("Toggle Don't Clear Screen"),
_trans("Toggle Force VBI"),
_trans("Toggle Remove Cinematic Bars"),
_trans("Toggle Shader Hunting"),
_trans("Cycle Hunting Option"),
_trans("Cycle Shader Type"),
_trans("Cycle Override Handling"),
_trans("Previous Shader"),
_trans("Next Shader"),
_trans("Previous Texture Hash"),
_trans("Next Texture Hash"),
_trans("Toggle Texture Hash Filter"),
_trans("Save Shader Override"),
// i18n: IR stands for internal resolution
_trans("Increase IR"),
// i18n: IR stands for internal resolution
@@ -311,6 +341,8 @@ constexpr std::array<HotkeyGroupInfo, NUM_HOTKEY_GROUPS> s_groups_info = {
{_trans("Controller Profile 3"), HK_NEXT_WIIMOTE_PROFILE_3, HK_PREV_GAME_WIIMOTE_PROFILE_3},
{_trans("Controller Profile 4"), HK_NEXT_WIIMOTE_PROFILE_4, HK_PREV_GAME_WIIMOTE_PROFILE_4},
{_trans("Graphics Toggles"), HK_TOGGLE_CROP, HK_TOGGLE_TEXTURES},
{_trans("VR"), HK_VR_TOGGLE_OPENXR, HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS},
{_trans("VR Shader"), HK_VR_SHADER_TOGGLE_HUNTING, HK_VR_SHADER_SAVE_OVERRIDE},
{_trans("Internal Resolution"), HK_INCREASE_IR, HK_DECREASE_IR},
{_trans("Freelook"), HK_FREELOOK_TOGGLE, HK_FREELOOK_TOGGLE},
// i18n: Stereoscopic 3D
+32
View File
@@ -102,6 +102,36 @@ enum Hotkey
HK_TOGGLE_DUMPTEXTURES,
HK_TOGGLE_TEXTURES,
HK_VR_TOGGLE_OPENXR,
HK_VR_RESET_POSITION,
HK_VR_DECREASE_UNITS_PER_METER,
HK_VR_INCREASE_UNITS_PER_METER,
HK_VR_DECREASE_LEAN_BACK_ANGLE,
HK_VR_INCREASE_LEAN_BACK_ANGLE,
HK_VR_DECREASE_CAMERA_FORWARD,
HK_VR_INCREASE_CAMERA_FORWARD,
HK_VR_TOGGLE_VIRTUAL_SCREEN,
HK_VR_DECREASE_SCREEN_DISTANCE,
HK_VR_INCREASE_SCREEN_DISTANCE,
HK_VR_DECREASE_SCREEN_SIZE,
HK_VR_INCREASE_SCREEN_SIZE,
HK_VR_DECREASE_SCREEN_CURVATURE,
HK_VR_INCREASE_SCREEN_CURVATURE,
HK_VR_TOGGLE_DONT_CLEAR_SCREEN,
HK_VR_TOGGLE_FORCE_VBI,
HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS,
HK_VR_SHADER_TOGGLE_HUNTING,
HK_VR_SHADER_CYCLE_HUNTING_OPTION,
HK_VR_SHADER_CYCLE_TYPE,
HK_VR_SHADER_CYCLE_HANDLING,
HK_VR_SHADER_PREV,
HK_VR_SHADER_NEXT,
HK_VR_SHADER_PREV_TEXTURE_HASH,
HK_VR_SHADER_NEXT_TEXTURE_HASH,
HK_VR_SHADER_TOGGLE_TEXTURE_HASH,
HK_VR_SHADER_SAVE_OVERRIDE,
HK_INCREASE_IR,
HK_DECREASE_IR,
@@ -205,6 +235,8 @@ enum HotkeyGroup : int
HKGP_CONTROLLER_PROFILE_3,
HKGP_CONTROLLER_PROFILE_4,
HKGP_GRAPHICS_TOGGLES,
HKGP_VR,
HKGP_VR_SHADER,
HKGP_IR,
HKGP_FREELOOK,
HKGP_3D_TOGGLE,
+51 -21
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <filesystem>
#include <future>
#include <locale>
#include <map>
#include <mutex>
@@ -87,6 +88,7 @@ struct CompressAndDumpStateArgs
{
Common::UniqueBuffer<u8> buffer;
std::string filename;
CompressionType compression_type = CompressionType::LZ4;
std::shared_lock<decltype(s_state_saves_in_progress)> task_lock;
};
@@ -121,8 +123,6 @@ static const std::map<u32, std::pair<std::string, std::string>> s_old_versions =
{38, {"4.0-4963", "4.0-5267"}}, {39, {"4.0-5279", "4.0-5525"}}, {40, {"4.0-5531", "4.0-5809"}},
{41, {"4.0-5811", "4.0-5923"}}, {42, {"4.0-5925", "4.0-5946"}}};
static constexpr bool s_use_compression = true;
// Acquired for tasks that will write state save data to the filesystem.
// This allows for later waiting on completion of said tasks when necessary.
// We want to maintain a proper order of async operations, e.g. Save, Save, GetInfoString.
@@ -353,19 +353,25 @@ static void CompressBufferToFile(std::span<const u8> raw_buffer, File::IOFile& f
}
}
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size)
static CompressionType GetCompressionType(const SaveOptions& options)
{
return options.compress ? CompressionType::LZ4 : CompressionType::Uncompressed;
}
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size,
CompressionType compression_type)
{
StateExtendedBaseHeader& base_header = extended_header.base_header;
base_header.header_version = EXTENDED_HEADER_VERSION;
base_header.compression_type =
s_use_compression ? CompressionType::LZ4 : CompressionType::Uncompressed;
base_header.compression_type = compression_type;
base_header.payload_offset = COMPRESSED_DATA_OFFSET;
base_header.uncompressed_size = uncompressed_size;
// If more fields are added to StateExtendedHeader, set them here.
}
static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
static void WriteHeadersToFile(size_t uncompressed_size, CompressionType compression_type,
File::IOFile& f)
{
StateHeader header{};
SConfig::GetInstance().GetGameID().copy(header.legacy_header.game_id,
@@ -377,7 +383,7 @@ static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
header.version_header.version_string_length = static_cast<u32>(header.version_string.length());
StateExtendedHeader extended_header{};
CreateExtendedHeader(extended_header, uncompressed_size);
CreateExtendedHeader(extended_header, uncompressed_size, compression_type);
f.WriteArray(&header.legacy_header, 1);
f.WriteArray(&header.version_header, 1);
@@ -411,9 +417,9 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
return;
}
WriteHeadersToFile(buffer.size(), f);
WriteHeadersToFile(buffer.size(), save_args.compression_type, f);
if (s_use_compression)
if (save_args.compression_type == CompressionType::LZ4)
CompressBufferToFile(buffer, f);
else
f.WriteBytes(buffer.data(), buffer.size());
@@ -467,7 +473,7 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
}
}
static void SaveAsFromCore(Core::System& system, std::string filename)
static void SaveAsFromCore(Core::System& system, std::string filename, SaveOptions options)
{
// Try with a buffer a bit larger than the previous state.
// This will often avoid the "Measure" step.
@@ -482,10 +488,13 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
CompressAndDumpStateArgs dump_args{
.buffer = std::move(buffer),
.filename = std::move(filename),
.compression_type = GetCompressionType(options),
.task_lock = GetStateSaveTaskLock(),
};
Core::DisplayMessage("Saving State...", 1000);
s_compress_and_dump_thread.EmplaceItem(std::move(dump_args));
if (options.wait_for_completion)
s_compress_and_dump_thread.WaitForCompletion();
}
else
{
@@ -493,12 +502,28 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
}
}
void SaveAs(Core::System& system, std::string filename)
void SaveAs(Core::System& system, std::string filename, SaveOptions options)
{
Core::RunOnCPUThread(
system, [&system, filename = std::move(filename), lock = GetStateSaveTaskLock()]() mutable {
SaveAsFromCore(system, std::move(filename));
});
if (options.wait_for_completion)
{
std::promise<void> completion;
auto future = completion.get_future();
Core::RunOnCPUThread(system,
[&system, filename = std::move(filename), options,
lock = GetStateSaveTaskLock(),
completion = std::move(completion)]() mutable {
SaveAsFromCore(system, std::move(filename), options);
completion.set_value();
});
future.wait();
return;
}
Core::RunOnCPUThread(system,
[&system, filename = std::move(filename), options,
lock = GetStateSaveTaskLock()]() mutable {
SaveAsFromCore(system, std::move(filename), options);
});
}
static bool GetVersionFromLZO(StateHeader& header, File::IOFile& f)
@@ -807,7 +832,7 @@ static void LoadFileStateData(const std::string& filename, Common::UniqueBuffer<
static void LoadAsFromCore(Core::System& system, std::string filename)
{
// Ensure all data has reached the filesystem before trying to use it.
s_compress_and_dump_thread.WaitForCompletion();
WaitForPendingSaves();
// Save temp buffer for undo load state
auto& movie = system.GetMovie();
@@ -899,9 +924,9 @@ void Shutdown()
s_flush_unsaved_data_hook.reset();
}
void Save(Core::System& system, int slot)
void Save(Core::System& system, int slot, SaveOptions options)
{
SaveAs(system, MakeStateFilename(slot));
SaveAs(system, MakeStateFilename(slot), options);
}
void Load(Core::System& system, int slot)
@@ -909,6 +934,11 @@ void Load(Core::System& system, int slot)
LoadAs(system, MakeStateFilename(slot));
}
void WaitForPendingSaves()
{
s_compress_and_dump_thread.WaitForCompletion();
}
void LoadLastSaved(Core::System& system, int i)
{
if (!CheckIfStateLoadIsAllowed(system))
@@ -916,7 +946,7 @@ void LoadLastSaved(Core::System& system, int i)
Core::RunOnCPUThread(system, [&system, i] {
// Data must reach the filesystem for up to date "UsedSlots".
s_compress_and_dump_thread.WaitForCompletion();
WaitForPendingSaves();
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
if (std::size_t(i) > used_slots.size())
@@ -934,7 +964,7 @@ void SaveFirstSaved(Core::System& system)
{
Core::RunOnCPUThread(system, [&system, lock = GetStateSaveTaskLock()] {
// Data must reach the filesystem for up to date "UsedSlots".
s_compress_and_dump_thread.WaitForCompletion();
WaitForPendingSaves();
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
auto slot = GetEmptySlot(used_slots);
@@ -945,7 +975,7 @@ void SaveFirstSaved(Core::System& system)
slot = used_slots.front().slot;
}
SaveAsFromCore(system, MakeStateFilename(*slot));
SaveAsFromCore(system, MakeStateFilename(*slot), {});
});
}
+9 -2
View File
@@ -22,6 +22,12 @@ namespace State
// number of states
static const u32 NUM_STATES = 10;
struct SaveOptions
{
bool compress = true;
bool wait_for_completion = false;
};
struct StateHeaderLegacy
{
char game_id[6];
@@ -92,11 +98,12 @@ u64 GetUnixTimeOfSlot(int slot);
// These don't happen instantly - they get scheduled as events.
// ...But only if we're not in the main CPU thread.
// If we're in the main CPU thread then they run immediately instead.
void Save(Core::System& system, int slot);
void Save(Core::System& system, int slot, SaveOptions options = {});
void Load(Core::System& system, int slot);
void SaveAs(Core::System& system, std::string filename);
void SaveAs(Core::System& system, std::string filename, SaveOptions options = {});
void LoadAs(Core::System& system, std::string filename);
void WaitForPendingSaves();
void LoadLastSaved(Core::System& system, int i = 1);
void SaveFirstSaved(Core::System& system);
+30
View File
@@ -48,6 +48,16 @@ add_executable(dolphin-emu
Config/CheatCodeEditor.h
Config/CheatWarningWidget.cpp
Config/CheatWarningWidget.h
Config/ElementsGroupOverrideAddEditDialog.cpp
Config/ElementsGroupOverrideAddEditDialog.h
Config/ElementsGroupOverrideWidget.cpp
Config/ElementsGroupOverrideWidget.h
Config/ElementsHuntingDialog.cpp
Config/ElementsHuntingDialog.h
Config/ImportShaderFindElementDialog.cpp
Config/ImportShaderFindElementDialog.h
Config/TextureHashBrowserDialog.cpp
Config/TextureHashBrowserDialog.h
Config/CommonControllersWidget.cpp
Config/CommonControllersWidget.h
Config/ConfigControls/ConfigBool.cpp
@@ -91,6 +101,8 @@ add_executable(dolphin-emu
Config/GameConfigHighlighter.h
Config/GameConfigWidget.cpp
Config/GameConfigWidget.h
Config/VRConfigWidget.cpp
Config/VRConfigWidget.h
Config/GamecubeControllersWidget.cpp
Config/GamecubeControllersWidget.h
Config/GeckoCodeWidget.cpp
@@ -155,6 +167,8 @@ add_executable(dolphin-emu
Config/Mapping/HotkeyTAS.h
Config/Mapping/HotkeyUSBEmu.cpp
Config/Mapping/HotkeyUSBEmu.h
Config/Mapping/HotkeyVR.cpp
Config/Mapping/HotkeyVR.h
Config/Mapping/HotkeyWii.cpp
Config/Mapping/HotkeyWii.h
Config/Mapping/IOWindow.cpp
@@ -166,6 +180,8 @@ add_executable(dolphin-emu
Config/Mapping/MappingIndicator.h
Config/Mapping/MappingNumeric.cpp
Config/Mapping/MappingNumeric.h
Config/Mapping/OpenXRWiimoteConfigSessionController.cpp
Config/Mapping/OpenXRWiimoteConfigSessionController.h
Config/Mapping/MappingWidget.cpp
Config/Mapping/MappingWidget.h
Config/Mapping/MappingWindow.cpp
@@ -186,6 +202,14 @@ add_executable(dolphin-emu
Config/NewPatchDialog.h
Config/PatchesWidget.cpp
Config/PatchesWidget.h
Config/HideObjectAddEditDialog.cpp
Config/HideObjectAddEditDialog.h
Config/HideObjectWidget.cpp
Config/HideObjectWidget.h
Config/ShaderOverrideAddEditDialog.cpp
Config/ShaderOverrideAddEditDialog.h
Config/ShaderOverrideWidget.cpp
Config/ShaderOverrideWidget.h
Config/PropertiesDialog.cpp
Config/PropertiesDialog.h
Config/SettingsWindow.cpp
@@ -229,6 +253,10 @@ add_executable(dolphin-emu
Debugger/CodeViewWidget.h
Debugger/CodeWidget.cpp
Debugger/CodeWidget.h
Debugger/CullingCodeFinderWidget.cpp
Debugger/CullingCodeFinderWidget.h
Debugger/ShaderHunterWidget.cpp
Debugger/ShaderHunterWidget.h
Debugger/EditSymbolDialog.cpp
Debugger/EditSymbolDialog.h
Debugger/GekkoSyntaxHighlight.cpp
@@ -382,6 +410,8 @@ add_executable(dolphin-emu
Settings/TriforcePane.h
Settings/USBDevicePicker.cpp
Settings/USBDevicePicker.h
Settings/VRPane.cpp
Settings/VRPane.h
Settings/WiiPane.cpp
Settings/WiiPane.h
SkylanderPortal/SkylanderModifyDialog.cpp
@@ -0,0 +1,878 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
#include <algorithm>
#include <cctype>
#include <unordered_map>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScrollArea>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QToolButton>
#include <QVBoxLayout>
#include <fmt/format.h>
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
namespace
{
constexpr double UPM_OVERRIDE_MIN = 0.01;
constexpr double UPM_OVERRIDE_MAX = 1000.0;
constexpr double UPM_OVERRIDE_STEP = 0.01;
constexpr int MAX_VISIBLE_TEXTURE_HASH_ROWS = 10;
constexpr int TEXTURE_HASH_ROW_HEIGHT = 28;
QString ToHashHex(u64 hash)
{
return QStringLiteral("%1").arg(static_cast<qulonglong>(hash), 16, 16, QLatin1Char('0'));
}
QString FormatRuntimeElementSummary(const ShaderHunter::RuntimeElementSignature& sig)
{
if (!sig.valid)
return QObject::tr("No runtime element captured.");
QStringList parts;
parts << QObject::tr("Projection: %1")
.arg(sig.perspective ? QObject::tr("Perspective") : QObject::tr("Orthographic"));
if (sig.perspective)
{
parts << QObject::tr("FOV: %1 / %2")
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
parts << QObject::tr("Near/Far: %1 / %2")
.arg(sig.perspective_near_x1000 / 1000.0, 0, 'f', 3)
.arg(sig.perspective_far_x100 / 100.0, 0, 'f', 2);
}
else
{
parts << QObject::tr("Ortho: L%1 R%2 T%3 B%4")
.arg(sig.ortho_left_x100 / 100.0, 0, 'f', 2)
.arg(sig.ortho_right_x100 / 100.0, 0, 'f', 2)
.arg(sig.ortho_top_x100 / 100.0, 0, 'f', 2)
.arg(sig.ortho_bottom_x100 / 100.0, 0, 'f', 2);
parts << QObject::tr("Layer: %1").arg(sig.ortho_layer);
}
parts << QObject::tr("Viewport: %1,%2 %3x%4")
.arg(sig.viewport_x)
.arg(sig.viewport_y)
.arg(sig.viewport_width)
.arg(sig.viewport_height);
parts << QObject::tr("Scissor: %1,%2 %3,%4")
.arg(sig.scissor_left)
.arg(sig.scissor_top)
.arg(sig.scissor_right)
.arg(sig.scissor_bottom);
parts << QObject::tr("State: alpha=%1 ztest=%2 zupdate=%3 zfunc=%4 blend=%5/%6")
.arg(QStringLiteral("0x%1").arg(sig.alpha_test_hex, 8, 16, QLatin1Char('0')))
.arg(sig.ztest ? QObject::tr("on") : QObject::tr("off"))
.arg(sig.zupdate ? QObject::tr("on") : QObject::tr("off"))
.arg(sig.zfunc)
.arg(sig.blend_color_update ? QObject::tr("col") : QObject::tr("-"))
.arg(sig.blend_alpha_update ? QObject::tr("alpha") : QObject::tr("-"));
return parts.join(QStringLiteral("\n"));
}
QString FormatTextureSummary(const std::vector<u64>& textures)
{
if (textures.empty())
return QObject::tr("No textures");
QStringList parts;
const int shown = std::min<int>(2, static_cast<int>(textures.size()));
for (int i = 0; i < shown; ++i)
{
parts << QStringLiteral("%1")
.arg(static_cast<qulonglong>(textures[static_cast<size_t>(i)]), 16, 16,
QLatin1Char('0'));
}
if (static_cast<int>(textures.size()) > shown)
parts << QObject::tr("+%1 more").arg(static_cast<int>(textures.size()) - shown);
return parts.join(QStringLiteral(", "));
}
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;
}
QString FormatSelectedMatchSummary(const ElementsGroupManager::SelectedSubgroupSignature& sig)
{
QString label = QObject::tr("F %1/%2/%3")
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(sig.vs_family), 16, 8, QLatin1Char('0')))
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(sig.ps_family), 16, 8, QLatin1Char('0')))
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(sig.gs_family), 16, 8, QLatin1Char('0')));
label += QObject::tr(" | Tex %1").arg(FormatTextureSummary(sig.texture_hashes));
return label;
}
std::vector<TextureHashBrowserEntry> CollectTextureBrowserEntries(
const std::vector<ElementsGroupManager::DrawRecord>& draws)
{
std::vector<TextureHashBrowserEntry> entries;
std::unordered_map<u64, size_t> indices;
for (const auto& draw : draws)
{
for (size_t i = 0; i < draw.textures.size(); ++i)
{
const u64 hash = draw.textures[i];
if (hash == 0)
continue;
const auto it = indices.find(hash);
if (it == indices.end())
{
indices.emplace(hash, entries.size());
entries.push_back(TextureHashBrowserEntry{.hash = hash, .name = draw.texture_names[i]});
}
else if (entries[it->second].name.empty() && !draw.texture_names[i].empty())
{
entries[it->second].name = draw.texture_names[i];
}
}
}
std::sort(entries.begin(), entries.end(),
[](const TextureHashBrowserEntry& a, const TextureHashBrowserEntry& b) {
return a.hash < b.hash;
});
return entries;
}
} // namespace
ElementsGroupOverrideAddEditDialog::ElementsGroupOverrideAddEditDialog(
QWidget* parent, const ElementsGroupManager::ElementGroupOverride* edit_override,
const std::vector<std::string>& available_flags,
const ShaderHunter::ShaderOverride* import_shader,
const ElementsGroupManager::RuntimeElementSignature* import_runtime_element)
: QDialog(parent), m_available_flags(available_flags)
{
setWindowTitle(edit_override ? tr("Edit Elements Group Override") :
tr("Add Elements Group Override"));
setMinimumWidth(420);
m_name_edit = new QLineEdit;
m_name_edit->setPlaceholderText(tr("Override name..."));
m_comments_edit = new QPlainTextEdit;
m_comments_edit->setPlaceholderText(tr("Optional notes..."));
m_comments_edit->setTabChangesFocus(true);
m_comments_edit->setMinimumHeight(70);
m_credits_edit = new QLineEdit;
m_credits_edit->setPlaceholderText(tr("Optional author/credits..."));
m_handling_combo = new QComboBox;
m_handling_combo->addItem(tr("Skip"),
static_cast<int>(ElementsGroupManager::HandlingType::Skip));
m_handling_combo->addItem(tr("Screen"),
static_cast<int>(ElementsGroupManager::HandlingType::Screen));
m_handling_combo->addItem(tr("Fullscreen"),
static_cast<int>(ElementsGroupManager::HandlingType::Fullscreen));
m_handling_combo->addItem(tr("Fullscreen Mono"),
static_cast<int>(ElementsGroupManager::HandlingType::FullscreenMono));
m_handling_combo->addItem(tr("Head Locked"),
static_cast<int>(ElementsGroupManager::HandlingType::HeadLocked));
m_handling_combo->addItem(tr("Flag"),
static_cast<int>(ElementsGroupManager::HandlingType::Flag));
m_handling_combo->addItem(
tr("Units per Meter"),
static_cast<int>(ElementsGroupManager::HandlingType::UnitsPerMeter));
m_layer_label = new QLabel(tr("Layer:"));
m_layer_spin = new QSpinBox;
m_layer_spin->setRange(-1, 255);
m_layer_spin->setSpecialValueText(tr("Default"));
m_layer_spin->setValue(-1);
m_element_depth_label = new QLabel(tr("Element Depth:"));
m_element_depth_spin = new QDoubleSpinBox;
m_element_depth_spin->setRange(-1.0, 1000.0);
m_element_depth_spin->setDecimals(3);
m_element_depth_spin->setSingleStep(0.1);
m_element_depth_spin->setSpecialValueText(tr("Default"));
m_element_depth_spin->setValue(-1.0);
m_units_per_meter_label = new QLabel(tr("Units per Meter:"));
m_units_per_meter_spin = new QDoubleSpinBox;
m_units_per_meter_spin->setRange(0.0, UPM_OVERRIDE_MAX);
m_units_per_meter_spin->setDecimals(2);
m_units_per_meter_spin->setSingleStep(UPM_OVERRIDE_STEP);
m_units_per_meter_spin->setSpecialValueText(tr("Default"));
m_units_per_meter_spin->setValue(0.0);
m_flag_label = new QLabel(tr("Flag Group:"));
m_flag_edit = new QLineEdit;
m_flag_edit->setPlaceholderText(tr("e.g. thermal_visor"));
m_condition_label = new QLabel(tr("Condition Flag:"));
m_condition_combo = new QComboBox;
m_condition_combo->setEditable(true);
m_condition_combo->addItem(QString());
for (const std::string& flag : m_available_flags)
m_condition_combo->addItem(QString::fromStdString(flag));
m_condition_mode_label = new QLabel(tr("Condition Mode:"));
m_condition_mode_combo = new QComboBox;
m_condition_mode_combo->addItem(tr("Activate"), false);
m_condition_mode_combo->addItem(tr("Deactivate"), true);
m_element_filter_check = new QCheckBox(tr("Use Draw Call Range"));
m_element_start_label = new QLabel(tr("Draw Start:"));
m_element_start_spin = new QSpinBox;
m_element_start_spin->setRange(0, 1000000);
m_element_end_label = new QLabel(tr("Draw End:"));
m_element_end_spin = new QSpinBox;
m_element_end_spin->setRange(0, 1000000);
m_capture_seed_button = new QPushButton(tr("Use Current Hunt Seed"));
m_runtime_element_summary_label = new QLabel;
m_runtime_element_summary_label->setWordWrap(true);
m_runtime_use_projection_check = new QCheckBox(tr("Projection"));
m_runtime_use_layer_check = new QCheckBox(tr("Layer"));
m_runtime_use_viewport_check = new QCheckBox(tr("Viewport"));
m_runtime_use_scissor_check = new QCheckBox(tr("Scissor"));
m_runtime_use_render_state_check = new QCheckBox(tr("Render State"));
m_texture_mode_combo = new QComboBox;
m_texture_mode_combo->addItem(tr("Include"), false);
m_texture_mode_combo->addItem(tr("Exclude"), true);
m_selected_match_mode_combo = new QComboBox;
m_selected_match_mode_combo->addItem(tr("Include"), false);
m_selected_match_mode_combo->addItem(tr("Exclude"), true);
m_view_textures_button = new QPushButton(tr("View Textures"));
m_texture_hash_container = new QWidget;
m_texture_hash_layout = new QVBoxLayout(m_texture_hash_container);
m_texture_hash_layout->setContentsMargins(0, 0, 0, 0);
m_texture_hash_layout->setSpacing(4);
m_texture_hash_scroll = new QScrollArea;
m_texture_hash_scroll->setWidgetResizable(true);
m_texture_hash_scroll->setFrameShape(QFrame::NoFrame);
m_texture_hash_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_texture_hash_scroll->setWidget(m_texture_hash_container);
AddTextureHashField();
EnsureTextureHashFieldRows();
m_selected_match_list = new QListWidget;
m_add_current_match_button = new QPushButton(tr("Add Current Hunt Match"));
m_remove_selected_match_button = new QPushButton(tr("Remove Selected Match"));
m_refinement_toggle = new QToolButton;
m_refinement_toggle->setText(tr("Advanced Shader Refinement"));
m_refinement_toggle->setCheckable(true);
m_refinement_toggle->setChecked(false);
m_refinement_toggle->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_refinement_toggle->setArrowType(Qt::RightArrow);
m_refinement_frame = new QFrame;
m_refinement_frame->setVisible(false);
auto* refinement_form = new QFormLayout(m_refinement_frame);
m_refinement_enable_check = new QCheckBox(tr("Enable shader refinement"));
refinement_form->addRow(m_refinement_enable_check);
m_refinement_type_combo = new QComboBox;
m_refinement_type_combo->addItem(tr("Pixel Shader"),
static_cast<int>(ElementsGroupManager::ShaderType::Pixel));
m_refinement_type_combo->addItem(tr("Vertex Shader"),
static_cast<int>(ElementsGroupManager::ShaderType::Vertex));
m_refinement_type_combo->addItem(tr("Geometry Shader"),
static_cast<int>(ElementsGroupManager::ShaderType::Geometry));
refinement_form->addRow(tr("Shader Type:"), m_refinement_type_combo);
m_refinement_family_check = new QCheckBox(tr("Use Family Signature"));
refinement_form->addRow(m_refinement_family_check);
m_refinement_hash_label = new QLabel(tr("Exact Hash:"));
m_refinement_hash_edit = new QLineEdit;
m_refinement_hash_edit->setPlaceholderText(tr("0000000000000000"));
refinement_form->addRow(m_refinement_hash_label, m_refinement_hash_edit);
m_refinement_family_signature_label = new QLabel(tr("Family Signature:"));
m_refinement_family_signature_edit = new QLineEdit;
m_refinement_family_signature_edit->setPlaceholderText(tr("0000000000000000"));
refinement_form->addRow(m_refinement_family_signature_label, m_refinement_family_signature_edit);
auto* form = new QFormLayout;
form->addRow(tr("Name:"), m_name_edit);
form->addRow(tr("Handling:"), m_handling_combo);
form->addRow(m_layer_label, m_layer_spin);
form->addRow(m_element_depth_label, m_element_depth_spin);
form->addRow(m_units_per_meter_label, m_units_per_meter_spin);
form->addRow(m_flag_label, m_flag_edit);
form->addRow(m_condition_label, m_condition_combo);
form->addRow(m_condition_mode_label, m_condition_mode_combo);
form->addRow(m_element_filter_check);
form->addRow(m_element_start_label, m_element_start_spin);
form->addRow(m_element_end_label, m_element_end_spin);
form->addRow(QString(), m_view_textures_button);
form->addRow(tr("Texture Mode:"), m_texture_mode_combo);
form->addRow(tr("Texture Filters:"), m_texture_hash_scroll);
form->addRow(tr("Selected Match Mode:"), m_selected_match_mode_combo);
form->addRow(tr("Selected Matches:"), m_selected_match_list);
auto* runtime_group_row = new QHBoxLayout;
runtime_group_row->addWidget(m_runtime_use_projection_check);
runtime_group_row->addWidget(m_runtime_use_layer_check);
runtime_group_row->addWidget(m_runtime_use_viewport_check);
runtime_group_row->addWidget(m_runtime_use_scissor_check);
runtime_group_row->addWidget(m_runtime_use_render_state_check);
auto* layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(m_capture_seed_button);
layout->addWidget(m_runtime_element_summary_label);
layout->addLayout(runtime_group_row);
auto* selected_match_buttons = new QHBoxLayout;
selected_match_buttons->addWidget(m_add_current_match_button);
selected_match_buttons->addWidget(m_remove_selected_match_button);
layout->addLayout(selected_match_buttons);
layout->addWidget(m_refinement_toggle);
layout->addWidget(m_refinement_frame);
auto* notes_form = new QFormLayout;
notes_form->addRow(tr("Comments:"), m_comments_edit);
notes_form->addRow(tr("Credits:"), m_credits_edit);
layout->addLayout(notes_form);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this,
&ElementsGroupOverrideAddEditDialog::OnAccept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_handling_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ElementsGroupOverrideAddEditDialog::RefreshHandlingUi);
connect(m_element_filter_check, &QCheckBox::toggled, this, [this](bool checked) {
m_element_start_label->setVisible(checked);
m_element_start_spin->setVisible(checked);
m_element_end_label->setVisible(checked);
m_element_end_spin->setVisible(checked);
});
connect(m_capture_seed_button, &QPushButton::clicked, this,
&ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed);
connect(m_view_textures_button, &QPushButton::clicked, this,
&ElementsGroupOverrideAddEditDialog::ShowTextureBrowser);
connect(m_add_current_match_button, &QPushButton::clicked, this,
&ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch);
connect(m_remove_selected_match_button, &QPushButton::clicked, this,
&ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter);
connect(m_selected_match_list, &QListWidget::itemSelectionChanged, this, [this]() {
m_remove_selected_match_button->setEnabled(!m_selected_match_list->selectedItems().empty());
});
for (QCheckBox* checkbox : {m_runtime_use_projection_check, m_runtime_use_layer_check,
m_runtime_use_viewport_check, m_runtime_use_scissor_check,
m_runtime_use_render_state_check})
{
connect(checkbox, &QCheckBox::toggled, this, [this]() {
m_runtime_element.use_projection = m_runtime_use_projection_check->isChecked();
m_runtime_element.use_layer = m_runtime_use_layer_check->isChecked();
m_runtime_element.use_viewport = m_runtime_use_viewport_check->isChecked();
m_runtime_element.use_scissor = m_runtime_use_scissor_check->isChecked();
m_runtime_element.use_render_state = m_runtime_use_render_state_check->isChecked();
RefreshRuntimeElementSummary();
});
}
connect(m_refinement_toggle, &QToolButton::toggled, this, [this](bool checked) {
m_refinement_widgets_visible = checked;
m_refinement_toggle->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow);
m_refinement_frame->setVisible(checked);
});
connect(m_refinement_enable_check, &QCheckBox::toggled, this,
&ElementsGroupOverrideAddEditDialog::RefreshRefinementUi);
connect(m_refinement_family_check, &QCheckBox::toggled, this,
&ElementsGroupOverrideAddEditDialog::RefreshRefinementUi);
if (edit_override)
{
m_name_edit->setText(QString::fromStdString(edit_override->name));
m_comments_edit->setPlainText(QString::fromStdString(edit_override->comments));
m_credits_edit->setText(QString::fromStdString(edit_override->credits));
{
const int handling_idx = m_handling_combo->findData(static_cast<int>(edit_override->handling));
if (handling_idx >= 0)
m_handling_combo->setCurrentIndex(handling_idx);
}
m_layer_spin->setValue(edit_override->layer);
m_element_depth_spin->setValue(edit_override->element_depth);
if (edit_override->units_per_meter > 0.0f)
m_units_per_meter_spin->setValue(edit_override->units_per_meter);
m_flag_edit->setText(QString::fromStdString(edit_override->flag_group));
{
const QString condition = QString::fromStdString(edit_override->condition_flag);
int idx = m_condition_combo->findText(condition);
if (idx < 0 && !condition.isEmpty())
{
m_condition_combo->addItem(condition);
idx = m_condition_combo->findText(condition);
}
m_condition_combo->setCurrentIndex(std::max(idx, 0));
}
{
const int idx = m_condition_mode_combo->findData(edit_override->condition_inverted);
if (idx >= 0)
m_condition_mode_combo->setCurrentIndex(idx);
}
m_element_filter_check->setChecked(edit_override->element_start >= 0 &&
edit_override->element_end >= 0);
if (edit_override->element_start >= 0)
m_element_start_spin->setValue(edit_override->element_start);
if (edit_override->element_end >= 0)
m_element_end_spin->setValue(edit_override->element_end);
m_edit_element_reference_total = edit_override->element_reference_total;
{
const int idx = m_texture_mode_combo->findData(edit_override->texture_hashes_excluded);
if (idx >= 0)
m_texture_mode_combo->setCurrentIndex(idx);
}
SetTextureHashValues(edit_override->texture_hashes);
m_runtime_element = edit_override->runtime_element;
m_selected_match_filters = edit_override->selected_match_filter;
m_selected_match_filters_excluded = edit_override->selected_match_filter_excluded;
m_refinement_enable_check->setChecked(edit_override->refinement_enabled);
{
const int idx =
m_refinement_type_combo->findData(static_cast<int>(edit_override->refinement_type));
if (idx >= 0)
m_refinement_type_combo->setCurrentIndex(idx);
}
m_refinement_family_check->setChecked(edit_override->refinement_family_match);
m_refinement_hash_edit->setText(ToHashHex(edit_override->refinement_hash));
m_refinement_family_signature_edit->setText(
ToHashHex(edit_override->refinement_family_signature));
}
else if (import_shader)
{
const QString import_name = QString::fromStdString(import_shader->name).trimmed();
m_name_edit->setText(import_name.isEmpty() ? tr("Imported Element Override") :
tr("Imported from %1").arg(import_name));
{
const int handling_idx =
m_handling_combo->findData(static_cast<int>(import_shader->handling));
if (handling_idx >= 0)
m_handling_combo->setCurrentIndex(handling_idx);
}
{
const int idx = m_texture_mode_combo->findData(import_shader->texture_hashes_excluded);
if (idx >= 0)
m_texture_mode_combo->setCurrentIndex(idx);
}
SetTextureHashValues(import_shader->texture_hashes);
m_refinement_enable_check->setChecked(true);
{
const int idx =
m_refinement_type_combo->findData(static_cast<int>(import_shader->type));
if (idx >= 0)
m_refinement_type_combo->setCurrentIndex(idx);
}
m_refinement_family_check->setChecked(import_shader->match_mode ==
ShaderHunter::MatchMode::ShaderFamily &&
import_shader->hash_family_match);
m_refinement_hash_edit->setText(ToHashHex(import_shader->hash));
m_refinement_family_signature_edit->setText(ToHashHex(import_shader->family_signature));
if (m_refinement_enable_check->isChecked())
m_refinement_toggle->setChecked(true);
}
else
{
m_name_edit->setText(tr("Element Override"));
}
for (const auto& filter : m_selected_match_filters)
m_selected_match_list->addItem(FormatSelectedMatchSummary(filter));
{
const int idx = m_selected_match_mode_combo->findData(m_selected_match_filters_excluded);
if (idx >= 0)
m_selected_match_mode_combo->setCurrentIndex(idx);
}
m_remove_selected_match_button->setEnabled(false);
RefreshHandlingUi();
m_element_start_label->setVisible(m_element_filter_check->isChecked());
m_element_start_spin->setVisible(m_element_filter_check->isChecked());
m_element_end_label->setVisible(m_element_filter_check->isChecked());
m_element_end_spin->setVisible(m_element_filter_check->isChecked());
RefreshRuntimeElementSummary();
RefreshRefinementUi();
if (import_runtime_element != nullptr && import_runtime_element->valid)
{
m_runtime_element = *import_runtime_element;
RefreshRuntimeElementSummary();
}
}
ElementsGroupManager::ElementGroupOverride ElementsGroupOverrideAddEditDialog::GetResult() const
{
ElementsGroupManager::ElementGroupOverride result;
result.name = m_name_edit->text().trimmed().toStdString();
result.comments = m_comments_edit->toPlainText().trimmed().toStdString();
result.credits = m_credits_edit->text().trimmed().toStdString();
result.handling = static_cast<ElementsGroupManager::HandlingType>(
m_handling_combo->currentData().toInt());
result.runtime_element = m_runtime_element;
result.layer = m_layer_spin->value();
result.element_depth = m_element_depth_spin->value();
result.units_per_meter = m_units_per_meter_spin->value() > 0.0 ? m_units_per_meter_spin->value() :
-1.0f;
result.texture_hashes = CollectTextureHashValues();
result.texture_hashes_excluded =
!result.texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
result.selected_match_filter = m_selected_match_filters;
result.selected_match_filter_excluded =
!result.selected_match_filter.empty() && m_selected_match_mode_combo->currentData().toBool();
result.flag_group = m_flag_edit->text().trimmed().toStdString();
result.condition_flag = m_condition_combo->currentText().trimmed().toStdString();
result.condition_inverted =
!result.condition_flag.empty() && m_condition_mode_combo->currentData().toBool();
if (m_element_filter_check->isChecked())
{
result.element_start = m_element_start_spin->value();
result.element_end = m_element_end_spin->value();
result.element_reference_total = std::max(m_edit_element_reference_total, result.element_end + 1);
}
result.refinement_enabled = m_refinement_enable_check->isChecked();
result.refinement_type = static_cast<ElementsGroupManager::ShaderType>(
m_refinement_type_combo->currentData().toInt());
result.refinement_family_match = m_refinement_family_check->isChecked();
result.refinement_hash = std::strtoull(m_refinement_hash_edit->text().trimmed().toStdString().c_str(),
nullptr, 16);
result.refinement_family_signature = std::strtoull(
m_refinement_family_signature_edit->text().trimmed().toStdString().c_str(), nullptr, 16);
return result;
}
std::vector<u64> ElementsGroupOverrideAddEditDialog::CollectTextureHashValues() const
{
std::vector<u64> hashes;
hashes.reserve(m_texture_hash_edits.size());
for (QLineEdit* edit : m_texture_hash_edits)
{
const QString text = edit->text().trimmed();
if (text.isEmpty())
continue;
const u64 parsed = std::strtoull(text.toStdString().c_str(), nullptr, 16);
if (parsed != 0)
hashes.push_back(parsed);
}
std::sort(hashes.begin(), hashes.end());
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
return hashes;
}
void ElementsGroupOverrideAddEditDialog::SetTextureHashValues(const std::vector<u64>& hashes)
{
m_updating_texture_hash_fields = true;
while (m_texture_hash_edits.size() < hashes.size())
AddTextureHashField();
for (size_t i = 0; i < hashes.size(); ++i)
m_texture_hash_edits[i]->setText(ToHashHex(hashes[i]));
for (size_t i = hashes.size(); i < m_texture_hash_edits.size(); ++i)
m_texture_hash_edits[i]->clear();
m_updating_texture_hash_fields = false;
EnsureTextureHashFieldRows();
}
void ElementsGroupOverrideAddEditDialog::AddTextureHashField(const QString& text)
{
auto* edit = new QLineEdit;
edit->setPlaceholderText(tr("0000000000000000"));
edit->setText(text);
connect(edit, &QLineEdit::editingFinished, this,
[this, edit]() { SanitizeTextureHashField(edit); });
connect(edit, &QLineEdit::textChanged, this,
[this]() { EnsureTextureHashFieldRows(); });
m_texture_hash_layout->addWidget(edit);
m_texture_hash_edits.push_back(edit);
}
void ElementsGroupOverrideAddEditDialog::EnsureTextureHashFieldRows()
{
if (m_updating_texture_hash_fields)
return;
m_updating_texture_hash_fields = true;
int filled = 0;
for (QLineEdit* edit : m_texture_hash_edits)
{
if (!edit->text().trimmed().isEmpty())
++filled;
}
const int desired_rows = std::max(1, filled + 1);
while (static_cast<int>(m_texture_hash_edits.size()) < desired_rows)
AddTextureHashField();
while (static_cast<int>(m_texture_hash_edits.size()) > desired_rows)
{
QLineEdit* edit = m_texture_hash_edits.back();
if (!edit->text().trimmed().isEmpty())
break;
m_texture_hash_edits.pop_back();
m_texture_hash_layout->removeWidget(edit);
delete edit;
}
const int visible_rows = std::min<int>(desired_rows, MAX_VISIBLE_TEXTURE_HASH_ROWS);
const int viewport_height = visible_rows * TEXTURE_HASH_ROW_HEIGHT;
m_texture_hash_scroll->setMinimumHeight(viewport_height);
m_texture_hash_scroll->setMaximumHeight(viewport_height);
m_updating_texture_hash_fields = false;
}
void ElementsGroupOverrideAddEditDialog::SanitizeTextureHashField(QLineEdit* edit)
{
if (edit == nullptr || m_updating_texture_hash_fields)
return;
QString text = edit->text().trimmed().toLower();
QString filtered;
filtered.reserve(text.size());
for (const QChar c : text)
{
if (c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f')))
filtered.append(c);
}
if (filtered.size() > 16)
filtered = filtered.right(16);
edit->setText(filtered);
}
void ElementsGroupOverrideAddEditDialog::RefreshRuntimeElementSummary()
{
QString summary = FormatRuntimeElementSummary(m_runtime_element);
QStringList groups;
if (m_runtime_element.use_projection)
groups << tr("Projection");
if (m_runtime_element.use_layer)
groups << tr("Layer");
if (m_runtime_element.use_viewport)
groups << tr("Viewport");
if (m_runtime_element.use_scissor)
groups << tr("Scissor");
if (m_runtime_element.use_render_state)
groups << tr("Render State");
summary += tr("\nActive Groups: %1")
.arg(groups.isEmpty() ? tr("(none)") : groups.join(QStringLiteral(", ")));
m_runtime_element_summary_label->setText(summary);
const QSignalBlocker projection_blocker(m_runtime_use_projection_check);
const QSignalBlocker layer_blocker(m_runtime_use_layer_check);
const QSignalBlocker viewport_blocker(m_runtime_use_viewport_check);
const QSignalBlocker scissor_blocker(m_runtime_use_scissor_check);
const QSignalBlocker render_blocker(m_runtime_use_render_state_check);
m_runtime_use_projection_check->setChecked(m_runtime_element.use_projection);
m_runtime_use_layer_check->setChecked(m_runtime_element.use_layer);
m_runtime_use_viewport_check->setChecked(m_runtime_element.use_viewport);
m_runtime_use_scissor_check->setChecked(m_runtime_element.use_scissor);
m_runtime_use_render_state_check->setChecked(m_runtime_element.use_render_state);
}
void ElementsGroupOverrideAddEditDialog::RefreshHandlingUi()
{
const auto handling = static_cast<ElementsGroupManager::HandlingType>(
m_handling_combo->currentData().toInt());
const bool show_layer = (handling == ElementsGroupManager::HandlingType::Screen ||
handling == ElementsGroupManager::HandlingType::HeadLocked);
const bool show_units_per_meter =
(handling == ElementsGroupManager::HandlingType::UnitsPerMeter);
const bool is_flag = (handling == ElementsGroupManager::HandlingType::Flag);
m_layer_label->setVisible(show_layer);
m_layer_spin->setVisible(show_layer);
m_element_depth_label->setVisible(show_layer);
m_element_depth_spin->setVisible(show_layer);
m_units_per_meter_label->setVisible(show_units_per_meter);
m_units_per_meter_spin->setVisible(show_units_per_meter);
m_condition_label->setVisible(!is_flag);
m_condition_combo->setVisible(!is_flag);
m_condition_mode_label->setVisible(!is_flag);
m_condition_mode_combo->setVisible(!is_flag);
}
void ElementsGroupOverrideAddEditDialog::RefreshRefinementUi()
{
const bool enabled = m_refinement_enable_check->isChecked();
const bool use_family = enabled && m_refinement_family_check->isChecked();
m_refinement_type_combo->setEnabled(enabled);
m_refinement_family_check->setEnabled(enabled);
m_refinement_hash_label->setVisible(enabled && !use_family);
m_refinement_hash_edit->setVisible(enabled && !use_family);
m_refinement_family_signature_label->setVisible(enabled && use_family);
m_refinement_family_signature_edit->setVisible(enabled && use_family);
}
void ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed()
{
const auto status = ElementsGroupManager::GetInstance().GetStatus();
if (!status.seed_valid)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("No active Elements Hunting seed is available."));
return;
}
m_runtime_element = status.seed_signature;
RefreshRuntimeElementSummary();
}
void ElementsGroupOverrideAddEditDialog::ShowTextureBrowser()
{
const auto status = ElementsGroupManager::GetInstance().GetStatus();
if (!status.seed_valid)
{
QMessageBox::warning(this, tr("View Textures"),
tr("No active Elements Hunting preview scope is available.\n"
"Open Elements Hunting and select a seed first."));
return;
}
TextureHashBrowserConfig browser_config;
browser_config.title = tr("Textures for Current Element Selection");
browser_config.empty_info_text =
tr("No textures were captured yet for the current preview scope.");
browser_config.current_label = tr("current preview scope");
browser_config.fetch_current_label = []() {
const auto current_status = ElementsGroupManager::GetInstance().GetStatus();
return current_status.selected_match_filter_count > 0 ?
(current_status.selected_match_filter_excluded ? QObject::tr("unchecked matches only") :
QObject::tr("checked matches only")) :
QObject::tr("whole current group");
};
browser_config.initial_selected_hashes = CollectTextureHashValues();
browser_config.fetch_current_entries = []() {
return CollectTextureBrowserEntries(
ElementsGroupManager::GetInstance().GetCurrentTextureSourceDraws());
};
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
SetTextureHashValues(hashes);
};
ShowTextureHashBrowserDialog(this, browser_config);
}
void ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch()
{
const auto status = ElementsGroupManager::GetInstance().GetStatus();
if (!status.highlighted_draw)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("No active Elements Hunting match is available."));
return;
}
const auto matches = ElementsGroupManager::GetInstance().GetCurrentMatches();
if (status.selected_match < 0 || status.selected_match >= static_cast<int>(matches.size()))
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("No stable Elements Hunting match is available."));
return;
}
const auto filter = matches[static_cast<size_t>(status.selected_match)].subgroup;
const auto it = std::find_if(m_selected_match_filters.begin(), m_selected_match_filters.end(),
[&filter](const ElementsGroupManager::SelectedSubgroupSignature& existing) {
return SelectedSubgroupSignaturesEqual(existing, filter);
});
if (it != m_selected_match_filters.end())
return;
m_selected_match_filters.push_back(filter);
m_selected_match_list->addItem(FormatSelectedMatchSummary(filter));
}
void ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter()
{
const auto items = m_selected_match_list->selectedItems();
if (items.empty())
return;
const int row = m_selected_match_list->row(items[0]);
if (row < 0 || row >= static_cast<int>(m_selected_match_filters.size()))
return;
m_selected_match_filters.erase(m_selected_match_filters.begin() + row);
delete m_selected_match_list->takeItem(row);
}
void ElementsGroupOverrideAddEditDialog::OnAccept()
{
const auto result = GetResult();
if (result.name.empty())
{
QMessageBox::warning(this, tr("Elements Group Override"), tr("Name cannot be empty."));
return;
}
if (!result.runtime_element.valid)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("A runtime element signature is required."));
return;
}
if (!result.runtime_element.use_projection && !result.runtime_element.use_layer &&
!result.runtime_element.use_viewport && !result.runtime_element.use_scissor &&
!result.runtime_element.use_render_state)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("Enable at least one runtime signature group."));
return;
}
if (result.handling == ElementsGroupManager::HandlingType::Flag && result.flag_group.empty())
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("Flag handling requires a flag group name."));
return;
}
if (m_element_filter_check->isChecked() && m_element_end_spin->value() < m_element_start_spin->value())
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("Draw End must be greater than or equal to Draw Start."));
return;
}
if (result.refinement_enabled)
{
if (result.refinement_family_match)
{
if (result.refinement_family_signature == 0)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("Family refinement requires a non-zero family signature."));
return;
}
}
else if (result.refinement_hash == 0)
{
QMessageBox::warning(this, tr("Elements Group Override"),
tr("Exact refinement requires a non-zero shader hash."));
return;
}
}
accept();
}
@@ -0,0 +1,111 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <vector>
#include "VideoCommon/ElementsGroupManager.h"
class QCheckBox;
class QComboBox;
class QDoubleSpinBox;
class QFrame;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QListWidget;
class QScrollArea;
class QSpinBox;
class QToolButton;
class QVBoxLayout;
class QWidget;
class ElementsGroupOverrideAddEditDialog : public QDialog
{
Q_OBJECT
public:
explicit ElementsGroupOverrideAddEditDialog(
QWidget* parent,
const ElementsGroupManager::ElementGroupOverride* edit_override = nullptr,
const std::vector<std::string>& available_flags = {},
const ShaderHunter::ShaderOverride* import_shader = nullptr,
const ElementsGroupManager::RuntimeElementSignature* import_runtime_element = nullptr);
ElementsGroupManager::ElementGroupOverride GetResult() const;
private:
std::vector<u64> CollectTextureHashValues() const;
void SetTextureHashValues(const std::vector<u64>& hashes);
void AddTextureHashField(const QString& text = {});
void EnsureTextureHashFieldRows();
void SanitizeTextureHashField(QLineEdit* edit);
void ShowTextureBrowser();
void RefreshRuntimeElementSummary();
void RefreshHandlingUi();
void RefreshRefinementUi();
void CaptureCurrentSeed();
void AddCurrentHuntMatch();
void RemoveSelectedMatchFilter();
void OnAccept();
std::vector<std::string> m_available_flags;
int m_edit_element_reference_total = 0;
bool m_updating_texture_hash_fields = false;
bool m_refinement_widgets_visible = false;
QLineEdit* m_name_edit = nullptr;
QPlainTextEdit* m_comments_edit = nullptr;
QLineEdit* m_credits_edit = nullptr;
QComboBox* m_handling_combo = nullptr;
QSpinBox* m_layer_spin = nullptr;
QLabel* m_layer_label = nullptr;
QDoubleSpinBox* m_element_depth_spin = nullptr;
QLabel* m_element_depth_label = nullptr;
QDoubleSpinBox* m_units_per_meter_spin = nullptr;
QLabel* m_units_per_meter_label = nullptr;
QLineEdit* m_flag_edit = nullptr;
QLabel* m_flag_label = nullptr;
QComboBox* m_condition_combo = nullptr;
QLabel* m_condition_label = nullptr;
QComboBox* m_condition_mode_combo = nullptr;
QLabel* m_condition_mode_label = nullptr;
QCheckBox* m_element_filter_check = nullptr;
QSpinBox* m_element_start_spin = nullptr;
QLabel* m_element_start_label = nullptr;
QSpinBox* m_element_end_spin = nullptr;
QLabel* m_element_end_label = nullptr;
QPushButton* m_capture_seed_button = nullptr;
QLabel* m_runtime_element_summary_label = nullptr;
QCheckBox* m_runtime_use_projection_check = nullptr;
QCheckBox* m_runtime_use_layer_check = nullptr;
QCheckBox* m_runtime_use_viewport_check = nullptr;
QCheckBox* m_runtime_use_scissor_check = nullptr;
QCheckBox* m_runtime_use_render_state_check = nullptr;
QComboBox* m_texture_mode_combo = nullptr;
QComboBox* m_selected_match_mode_combo = nullptr;
QPushButton* m_view_textures_button = nullptr;
QWidget* m_texture_hash_container = nullptr;
QScrollArea* m_texture_hash_scroll = nullptr;
QVBoxLayout* m_texture_hash_layout = nullptr;
std::vector<QLineEdit*> m_texture_hash_edits;
QListWidget* m_selected_match_list = nullptr;
QPushButton* m_add_current_match_button = nullptr;
QPushButton* m_remove_selected_match_button = nullptr;
QToolButton* m_refinement_toggle = nullptr;
QFrame* m_refinement_frame = nullptr;
QCheckBox* m_refinement_enable_check = nullptr;
QComboBox* m_refinement_type_combo = nullptr;
QCheckBox* m_refinement_family_check = nullptr;
QLineEdit* m_refinement_hash_edit = nullptr;
QLabel* m_refinement_hash_label = nullptr;
QLineEdit* m_refinement_family_signature_edit = nullptr;
QLabel* m_refinement_family_signature_label = nullptr;
ElementsGroupManager::RuntimeElementSignature m_runtime_element;
std::vector<ElementsGroupManager::SelectedSubgroupSignature> m_selected_match_filters;
bool m_selected_match_filters_excluded = false;
};
@@ -0,0 +1,409 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ElementsGroupOverrideWidget.h"
#include <algorithm>
#include <optional>
#include <QDialog>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QVBoxLayout>
#include "Common/Logging/Log.h"
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
#include "DolphinQt/Config/ElementsHuntingDialog.h"
#include "DolphinQt/Config/ImportShaderFindElementDialog.h"
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
#include "VideoCommon/ShaderHunter.h"
namespace
{
QString FormatShaderOverrideImportLabel(const ShaderHunter::ShaderOverride& entry)
{
const QString type =
entry.type == ShaderHunter::ShaderType::Vertex ? QStringLiteral("VS") :
entry.type == ShaderHunter::ShaderType::Geometry ? QStringLiteral("GS") :
QStringLiteral("PS");
const QString handling =
entry.handling == ShaderHunter::HandlingType::Screen ? QStringLiteral("screen") :
entry.handling == ShaderHunter::HandlingType::Fullscreen ? QStringLiteral("fullscreen") :
entry.handling == ShaderHunter::HandlingType::FullscreenMono ? QStringLiteral("fullscreen_mono") :
entry.handling == ShaderHunter::HandlingType::HeadLocked ? QStringLiteral("headlocked") :
entry.handling == ShaderHunter::HandlingType::Flag ? QStringLiteral("flag") :
entry.handling == ShaderHunter::HandlingType::UnitsPerMeter ? QStringLiteral("units_per_meter") :
QStringLiteral("skip");
QString label = QStringLiteral("%1 [%2 %3]")
.arg(QString::fromStdString(entry.name), type, handling);
if (!entry.texture_hashes.empty())
label += QStringLiteral(" tex[%1]").arg(entry.texture_hashes.size());
if (!entry.enabled)
label += QStringLiteral(" off");
return label;
}
std::optional<ShaderHunter::ShaderOverride> SelectShaderOverrideForImport(
QWidget* parent, const std::vector<ShaderHunter::ShaderOverride>& overrides)
{
if (overrides.empty())
return std::nullopt;
QDialog dialog(parent);
dialog.setWindowTitle(QObject::tr("Import From Shader"));
dialog.setMinimumWidth(420);
auto* list = new QListWidget(&dialog);
for (size_t i = 0; i < overrides.size(); ++i)
{
auto* item = new QListWidgetItem(FormatShaderOverrideImportLabel(overrides[i]), list);
item->setData(Qt::UserRole, static_cast<int>(i));
}
list->setSelectionMode(QAbstractItemView::SingleSelection);
if (list->count() > 0)
list->setCurrentRow(0);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
auto* ok = buttons->button(QDialogButtonBox::Ok);
if (ok != nullptr)
ok->setText(QObject::tr("Import"));
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
auto* layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel(QObject::tr("Choose a saved Shader Override to import."), &dialog));
layout->addWidget(list);
layout->addWidget(buttons);
if (dialog.exec() != QDialog::Accepted || list->currentItem() == nullptr)
return std::nullopt;
const int index = list->currentItem()->data(Qt::UserRole).toInt();
if (index < 0 || index >= static_cast<int>(overrides.size()))
return std::nullopt;
return overrides[static_cast<size_t>(index)];
}
} // namespace
ElementsGroupOverrideWidget::ElementsGroupOverrideWidget(std::string game_id)
: m_game_id(std::move(game_id))
{
CreateWidgets();
ConnectWidgets();
LoadOverrides();
}
ElementsGroupOverrideWidget::~ElementsGroupOverrideWidget() = default;
void ElementsGroupOverrideWidget::CreateWidgets()
{
m_code_list = new QListWidget;
auto* info_label = new QLabel(
tr("Elements Group Overrides use captured runtime draw signatures instead of only shader "
"hashes.\nUse Elements Hunting to isolate frame-local draw groups, then save a new "
"override."));
info_label->setWordWrap(true);
m_code_add = new NonDefaultQPushButton(tr("&Add Override"));
m_code_import = new NonDefaultQPushButton(tr("Import from Shader"));
m_code_edit = new NonDefaultQPushButton(tr("&Edit Override"));
m_code_remove = new NonDefaultQPushButton(tr("&Remove Override"));
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
m_code_reload = new NonDefaultQPushButton(tr("Reload &Overrides"));
m_elements_hunting = new NonDefaultQPushButton(tr("Elements Hunting"));
m_code_edit->setEnabled(false);
m_code_remove->setEnabled(false);
auto* button_layout = new QHBoxLayout;
button_layout->addWidget(m_elements_hunting);
button_layout->addWidget(m_code_add);
button_layout->addWidget(m_code_import);
button_layout->addWidget(m_code_edit);
button_layout->addWidget(m_code_remove);
button_layout->addStretch();
button_layout->addWidget(m_code_refresh);
button_layout->addWidget(m_code_reload);
auto* layout = new QVBoxLayout(this);
layout->addWidget(info_label);
layout->addWidget(m_code_list);
layout->addLayout(button_layout);
}
void ElementsGroupOverrideWidget::ConnectWidgets()
{
connect(m_code_list, &QListWidget::itemChanged, this, &ElementsGroupOverrideWidget::OnItemChanged);
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
&ElementsGroupOverrideWidget::OnSelectionChanged);
connect(m_code_add, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnAddClicked);
connect(m_code_import, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnImportClicked);
connect(m_code_edit, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnEditClicked);
connect(m_code_remove, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnRemoveClicked);
connect(m_code_refresh, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnRefreshClicked);
connect(m_code_reload, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnReloadClicked);
connect(m_elements_hunting, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnHuntClicked);
}
void ElementsGroupOverrideWidget::LoadOverrides()
{
m_overrides = ElementsGroupManager::LoadOverridesFromINI(m_game_id);
UpdateList();
m_code_edit->setEnabled(false);
m_code_remove->setEnabled(false);
}
void ElementsGroupOverrideWidget::SaveOverrides()
{
ElementsGroupManager::SaveOverridesToINI(m_game_id, m_overrides);
}
void ElementsGroupOverrideWidget::UpdateList()
{
const QSignalBlocker blocker(m_code_list);
m_code_list->clear();
for (size_t i = 0; i < m_overrides.size(); ++i)
{
const auto& entry = m_overrides[i];
QString label = QStringLiteral("%1 (%2)")
.arg(QString::fromStdString(entry.name))
.arg(QString::fromLatin1(entry.handling == ShaderHunter::HandlingType::Screen ? "screen" :
entry.handling == ShaderHunter::HandlingType::Fullscreen ? "fullscreen" :
entry.handling == ShaderHunter::HandlingType::FullscreenMono ? "fullscreen_mono" :
entry.handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" :
entry.handling == ShaderHunter::HandlingType::Flag ? "flag" :
entry.handling == ShaderHunter::HandlingType::UnitsPerMeter ? "units_per_meter" :
"skip"));
if (entry.runtime_element.use_projection)
label += QStringLiteral(" proj");
if (entry.runtime_element.use_layer)
label += QStringLiteral(" layer");
if (entry.runtime_element.use_viewport)
label += QStringLiteral(" vp");
if (entry.runtime_element.use_scissor)
label += QStringLiteral(" scissor");
if (entry.runtime_element.use_render_state)
label += QStringLiteral(" state");
if (!entry.texture_hashes.empty())
label += QStringLiteral(" tex");
if (!entry.selected_match_filter.empty())
{
label += QStringLiteral(" %1[%2]")
.arg(entry.selected_match_filter_excluded ? QStringLiteral("match_exclude") :
QStringLiteral("matches"))
.arg(entry.selected_match_filter.size());
}
if (entry.element_start >= 0 && entry.element_end >= 0)
label += QStringLiteral(" draw[%1-%2]").arg(entry.element_start).arg(entry.element_end);
if (!entry.condition_flag.empty())
label += QStringLiteral(" when(%1%2)")
.arg(entry.condition_inverted ? QStringLiteral("!") : QString())
.arg(QString::fromStdString(entry.condition_flag));
if (entry.refinement_enabled)
{
const QString refine_type =
entry.refinement_type == ShaderHunter::ShaderType::Vertex ? QStringLiteral("VS") :
entry.refinement_type == ShaderHunter::ShaderType::Geometry ? QStringLiteral("GS") :
QStringLiteral("PS");
label += QStringLiteral(" refine[%1 %2]")
.arg(refine_type)
.arg(entry.refinement_family_match ? QStringLiteral("family") :
QStringLiteral("exact"));
}
auto* item = new QListWidgetItem(label);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
item->setCheckState(entry.enabled ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, static_cast<int>(i));
m_code_list->addItem(item);
}
}
std::vector<std::string> ElementsGroupOverrideWidget::CollectAvailableFlags() const
{
std::vector<std::string> flags;
const auto shader_overrides = ShaderHunter::LoadOverridesFromINI(m_game_id);
for (const auto& entry : shader_overrides)
{
if (!entry.flag_group.empty() &&
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
{
flags.push_back(entry.flag_group);
}
}
for (const auto& entry : m_overrides)
{
if (!entry.flag_group.empty() &&
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
{
flags.push_back(entry.flag_group);
}
}
return flags;
}
void ElementsGroupOverrideWidget::OnItemChanged(QListWidgetItem* item)
{
const int idx = item->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
m_overrides[idx].enabled = item->checkState() == Qt::Checked;
INFO_LOG_FMT(VIDEO, "ElementsGroup: override '{}' {}", m_overrides[idx].name,
m_overrides[idx].enabled ? "enabled" : "disabled");
SaveOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnSelectionChanged()
{
const bool has_selection = !m_code_list->selectedItems().empty();
m_code_edit->setEnabled(has_selection);
m_code_remove->setEnabled(has_selection);
}
void ElementsGroupOverrideWidget::OnAddClicked()
{
ElementsGroupOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags());
if (dialog.exec() != QDialog::Accepted)
return;
auto result = dialog.GetResult();
result.enabled = true;
result.user_defined = true;
m_overrides.push_back(std::move(result));
SaveOverrides();
LoadOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnImportClicked()
{
std::vector<ShaderHunter::ShaderOverride> supported_overrides;
for (const auto& entry : ShaderHunter::LoadOverridesFromINI(m_game_id))
{
if (entry.match_mode == ShaderHunter::MatchMode::RuntimeElement)
continue;
supported_overrides.push_back(entry);
}
if (supported_overrides.empty())
{
QMessageBox::information(this, tr("Import from Shader"),
tr("No compatible saved Shader Overrides were found."));
return;
}
const auto selected = SelectShaderOverrideForImport(this, supported_overrides);
if (!selected.has_value())
return;
ImportShaderFindElementDialog find_dialog(m_game_id, *selected, this);
const int result_code = find_dialog.exec();
if (find_dialog.ShouldOpenElementsHunting())
{
OnHuntClicked();
return;
}
if (result_code != QDialog::Accepted)
return;
const auto selected_candidate = find_dialog.GetSelectedCandidate();
if (!selected_candidate.has_value())
{
QMessageBox::warning(this, tr("Import from Shader"),
tr("No runtime element seed candidate was selected."));
return;
}
auto import_runtime_element =
ElementsGroupManager::MakeSelectedMatchFilterSignature(selected_candidate->signature);
ElementsGroupOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags(), &*selected,
&import_runtime_element);
if (dialog.exec() != QDialog::Accepted)
return;
auto result = dialog.GetResult();
result.enabled = true;
result.user_defined = true;
m_overrides.push_back(std::move(result));
SaveOverrides();
LoadOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnEditClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int idx = items[0]->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
ElementsGroupOverrideAddEditDialog dialog(this, &m_overrides[idx], CollectAvailableFlags());
if (dialog.exec() != QDialog::Accepted)
return;
auto result = dialog.GetResult();
result.enabled = m_overrides[idx].enabled;
m_overrides[idx] = std::move(result);
SaveOverrides();
LoadOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnRemoveClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int idx = items[0]->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
m_overrides.erase(m_overrides.begin() + idx);
SaveOverrides();
LoadOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnRefreshClicked()
{
LoadOverrides();
}
void ElementsGroupOverrideWidget::OnReloadClicked()
{
INFO_LOG_FMT(VIDEO, "ElementsGroup: reload requested for {}", m_game_id);
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
}
void ElementsGroupOverrideWidget::OnHuntClicked()
{
if (m_hunting_dialog)
{
m_hunting_dialog->show();
m_hunting_dialog->raise();
m_hunting_dialog->activateWindow();
return;
}
m_hunting_dialog = new ElementsHuntingDialog(m_game_id, this);
m_hunting_dialog->setAttribute(Qt::WA_DeleteOnClose, true);
connect(m_hunting_dialog, &ElementsHuntingDialog::OverridesChanged, this,
[this]() {
LoadOverrides();
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
});
connect(m_hunting_dialog, &QObject::destroyed, this,
[this]() { m_hunting_dialog = nullptr; });
m_hunting_dialog->show();
}
@@ -0,0 +1,55 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QPointer>
#include <QWidget>
#include <string>
#include <vector>
#include "VideoCommon/ElementsGroupManager.h"
class ElementsHuntingDialog;
class QListWidget;
class QListWidgetItem;
class QPushButton;
class ElementsGroupOverrideWidget : public QWidget
{
Q_OBJECT
public:
explicit ElementsGroupOverrideWidget(std::string game_id);
~ElementsGroupOverrideWidget() override;
private:
void CreateWidgets();
void ConnectWidgets();
void UpdateList();
void LoadOverrides();
void SaveOverrides();
std::vector<std::string> CollectAvailableFlags() const;
void OnItemChanged(QListWidgetItem* item);
void OnSelectionChanged();
void OnAddClicked();
void OnImportClicked();
void OnEditClicked();
void OnRemoveClicked();
void OnRefreshClicked();
void OnReloadClicked();
void OnHuntClicked();
std::string m_game_id;
QListWidget* m_code_list = nullptr;
QPushButton* m_code_add = nullptr;
QPushButton* m_code_import = nullptr;
QPushButton* m_code_edit = nullptr;
QPushButton* m_code_remove = nullptr;
QPushButton* m_code_refresh = nullptr;
QPushButton* m_code_reload = nullptr;
QPushButton* m_elements_hunting = nullptr;
QPointer<ElementsHuntingDialog> m_hunting_dialog;
std::vector<ElementsGroupManager::ElementGroupOverride> m_overrides;
};
@@ -0,0 +1,721 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ElementsHuntingDialog.h"
#include <algorithm>
#include <unordered_map>
#include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QAbstractItemView>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QTimer>
#include <QVBoxLayout>
#include <fmt/format.h>
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
#include "VideoCommon/ElementsGroupManager.h"
#include "VideoCommon/ShaderHunter.h"
namespace
{
QString GetProjectionLabel(const ShaderHunter::RuntimeElementSignature& sig)
{
if (sig.perspective)
{
return QObject::tr("Perspective %1x%2")
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
}
return QObject::tr("Ortho L%1")
.arg(sig.ortho_layer);
}
QString FormatSeedCandidateLabel(const ElementsGroupManager::SeedCandidate& candidate,
const ShaderHunter::RuntimeElementSignature& active_mask)
{
const auto& group = candidate.group_signature;
const auto& rep = candidate.representative_draw.signature;
QStringList parts;
if (!active_mask.use_projection && !active_mask.use_layer && !active_mask.use_viewport &&
!active_mask.use_scissor && !active_mask.use_render_state)
{
parts << GetProjectionLabel(candidate.signature);
parts << QObject::tr("VP %1,%2 %3x%4")
.arg(candidate.signature.viewport_x)
.arg(candidate.signature.viewport_y)
.arg(candidate.signature.viewport_width)
.arg(candidate.signature.viewport_height);
parts << QObject::tr("SC %1,%2 %3,%4")
.arg(candidate.signature.scissor_left)
.arg(candidate.signature.scissor_top)
.arg(candidate.signature.scissor_right)
.arg(candidate.signature.scissor_bottom);
}
else
{
if (active_mask.use_projection)
parts << GetProjectionLabel(group);
else if (active_mask.use_layer)
parts << (rep.perspective ? QObject::tr("Perspective") :
QObject::tr("Ortho L%1").arg(group.ortho_layer));
if (active_mask.use_viewport)
{
parts << QObject::tr("VP %1,%2 %3x%4")
.arg(group.viewport_x)
.arg(group.viewport_y)
.arg(group.viewport_width)
.arg(group.viewport_height);
}
if (active_mask.use_scissor)
{
parts << QObject::tr("SC %1,%2 %3,%4")
.arg(group.scissor_left)
.arg(group.scissor_top)
.arg(group.scissor_right)
.arg(group.scissor_bottom);
}
if (active_mask.use_render_state)
parts << QObject::tr("State");
}
parts << QObject::tr("Draw #%1").arg(candidate.representative_draw.draw_index + 1);
parts << QObject::tr("Seen %1x").arg(candidate.occurrence_count);
return parts.join(QStringLiteral(" | "));
}
QString FormatSeedSummary(const ElementsGroupManager::SeedCandidate& candidate,
const ShaderHunter::RuntimeElementSignature& active_mask)
{
return QObject::tr("%1\nRepresentative Draw: #%2\nOccurrences: %3")
.arg(FormatSeedCandidateLabel(candidate, active_mask))
.arg(candidate.representative_draw.draw_index + 1)
.arg(candidate.occurrence_count);
}
QString FormatMatchLabel(const ElementsGroupManager::DrawRecord& draw)
{
return QObject::tr("Draw #%1 | VS %2 | PS %3 | GS %4")
.arg(draw.draw_index + 1)
.arg(static_cast<qulonglong>(draw.vs_hash), 16, 16, QLatin1Char('0'))
.arg(static_cast<qulonglong>(draw.ps_hash), 16, 16, QLatin1Char('0'))
.arg(static_cast<qulonglong>(draw.gs_hash), 16, 16, QLatin1Char('0'));
}
QString FormatTextureSummary(const std::vector<u64>& textures)
{
if (textures.empty())
return QObject::tr("No textures");
QStringList parts;
const int shown = std::min<int>(2, static_cast<int>(textures.size()));
for (int i = 0; i < shown; ++i)
{
parts << QStringLiteral("%1")
.arg(static_cast<qulonglong>(textures[static_cast<size_t>(i)]), 16, 16,
QLatin1Char('0'));
}
if (static_cast<int>(textures.size()) > shown)
parts << QObject::tr("+%1 more").arg(static_cast<int>(textures.size()) - shown);
return parts.join(QStringLiteral(", "));
}
QString FormatCurrentMatchLabel(const ElementsGroupManager::CurrentMatchCandidate& candidate)
{
QString label = GetProjectionLabel(candidate.representative_draw.signature);
label += QObject::tr(" | F %1/%2/%3")
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(candidate.subgroup.vs_family), 16, 8,
QLatin1Char('0')))
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(candidate.subgroup.ps_family), 16, 8,
QLatin1Char('0')))
.arg(QStringLiteral("%1")
.arg(static_cast<qulonglong>(candidate.subgroup.gs_family), 16, 8,
QLatin1Char('0')));
label += QObject::tr(" | Tex %1").arg(FormatTextureSummary(candidate.subgroup.texture_hashes));
label += QObject::tr(" | x%1").arg(candidate.raw_draw_count);
if (candidate.representative_draw.draw_index >= 0)
label += QObject::tr(" | Draw #%1").arg(candidate.representative_draw.draw_index + 1);
if (!candidate.active_this_frame)
label += QObject::tr(" | Inactive");
return label;
}
QString GetPreviewScopeLabel(const ElementsGroupManager::Status& status)
{
if (status.selected_match_filter_count <= 0)
return QObject::tr("Whole current group");
return status.selected_match_filter_excluded ? QObject::tr("Unchecked matches only") :
QObject::tr("Checked matches only");
}
std::vector<TextureHashBrowserEntry> CollectTextureBrowserEntries(
const std::vector<ElementsGroupManager::DrawRecord>& draws)
{
std::vector<TextureHashBrowserEntry> entries;
std::unordered_map<u64, size_t> indices;
for (const auto& draw : draws)
{
for (size_t i = 0; i < draw.textures.size(); ++i)
{
const u64 hash = draw.textures[i];
if (hash == 0)
continue;
const auto it = indices.find(hash);
if (it == indices.end())
{
indices.emplace(hash, entries.size());
entries.push_back(TextureHashBrowserEntry{.hash = hash, .name = draw.texture_names[i]});
}
else if (entries[it->second].name.empty() && !draw.texture_names[i].empty())
{
entries[it->second].name = draw.texture_names[i];
}
}
}
std::sort(entries.begin(), entries.end(),
[](const TextureHashBrowserEntry& a, const TextureHashBrowserEntry& b) {
return a.hash < b.hash;
});
return entries;
}
std::vector<std::string> CollectAvailableFlags(const std::string& game_id)
{
std::vector<std::string> flags;
const auto shader_overrides = ShaderHunter::LoadOverridesFromINI(game_id);
for (const auto& entry : shader_overrides)
{
if (!entry.flag_group.empty() &&
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
{
flags.push_back(entry.flag_group);
}
}
const auto element_overrides = ElementsGroupManager::LoadOverridesFromINI(game_id);
for (const auto& entry : element_overrides)
{
if (!entry.flag_group.empty() &&
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
{
flags.push_back(entry.flag_group);
}
}
return flags;
}
} // namespace
ElementsHuntingDialog::ElementsHuntingDialog(std::string game_id, QWidget* parent)
: QDialog(parent), m_game_id(std::move(game_id))
{
setWindowTitle(tr("Elements Hunting"));
setAttribute(Qt::WA_DeleteOnClose);
setMinimumWidth(420);
ElementsGroupManager::GetInstance().SetPopupOpen(true);
if (!m_game_id.empty())
ElementsGroupManager::GetInstance().LoadOverridesIfNeeded(m_game_id);
CreateWidgets();
ConnectWidgets();
RefreshPendingTextureSummary();
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, &ElementsHuntingDialog::UpdateDisplay);
m_update_timer->start(100);
}
ElementsHuntingDialog::~ElementsHuntingDialog() = default;
void ElementsHuntingDialog::CreateWidgets()
{
auto* layout = new QVBoxLayout(this);
auto* info = new QLabel(
tr("Select a runtime-signature seed candidate, then enable signature groups to cycle "
"matching elements across the current frame.\n"
"Preview affects the whole current group until one or more matches are checked below. "
"After that, preview and saved overrides affect only the checked matches.\n"
"Seen matches stay in the list even when not drawn and are shown in grey."));
info->setWordWrap(true);
layout->addWidget(info);
m_enable_check = new QCheckBox(tr("Enable Group Hunt"));
layout->addWidget(m_enable_check);
auto* option_layout = new QHBoxLayout;
option_layout->addWidget(new QLabel(tr("Preview:")));
m_option_combo = new QComboBox;
m_option_combo->addItem(tr("Skip"), static_cast<int>(ShaderHunter::HuntingOption::Skip));
m_option_combo->addItem(tr("Pink"), static_cast<int>(ShaderHunter::HuntingOption::Pink));
option_layout->addWidget(m_option_combo);
option_layout->addWidget(new QLabel(tr("Match Mode:")));
m_match_filter_mode_combo = new QComboBox;
m_match_filter_mode_combo->addItem(tr("Include"), false);
m_match_filter_mode_combo->addItem(tr("Exclude"), true);
option_layout->addWidget(m_match_filter_mode_combo);
option_layout->addWidget(new QLabel(tr("Texture Mode:")));
m_texture_mode_combo = new QComboBox;
m_texture_mode_combo->addItem(tr("Include"), false);
m_texture_mode_combo->addItem(tr("Exclude"), true);
option_layout->addWidget(m_texture_mode_combo);
option_layout->addStretch();
layout->addLayout(option_layout);
layout->addWidget(new QLabel(tr("Seed Candidates:")));
m_seed_list = new QListWidget;
m_seed_list->setSelectionMode(QAbstractItemView::SingleSelection);
layout->addWidget(m_seed_list);
m_seed_label = new QLabel(tr("Seed: (none)"));
m_seed_label->setWordWrap(true);
layout->addWidget(m_seed_label);
auto* groups_layout = new QHBoxLayout;
m_projection_check = new QCheckBox(tr("Projection"));
m_layer_check = new QCheckBox(tr("Layer"));
m_viewport_check = new QCheckBox(tr("Viewport"));
m_scissor_check = new QCheckBox(tr("Scissor"));
m_render_state_check = new QCheckBox(tr("Render State"));
groups_layout->addWidget(m_projection_check);
groups_layout->addWidget(m_layer_check);
groups_layout->addWidget(m_viewport_check);
groups_layout->addWidget(m_scissor_check);
groups_layout->addWidget(m_render_state_check);
layout->addLayout(groups_layout);
m_match_label = new QLabel(tr("Matches: - / -"));
layout->addWidget(m_match_label);
auto* match_nav = new QHBoxLayout;
m_prev_match_button = new QPushButton(tr("<< Previous Match"));
m_next_match_button = new QPushButton(tr("Next Match >>"));
match_nav->addWidget(m_prev_match_button);
match_nav->addWidget(m_next_match_button);
layout->addLayout(match_nav);
layout->addWidget(new QLabel(tr("Current Matches:")));
m_current_match_list = new QListWidget;
m_current_match_list->setSelectionMode(QAbstractItemView::SingleSelection);
layout->addWidget(m_current_match_list);
auto* texture_layout = new QHBoxLayout;
m_view_textures_button = new QPushButton(tr("View Textures"));
texture_layout->addWidget(m_view_textures_button);
texture_layout->addStretch();
layout->addLayout(texture_layout);
m_pending_texture_label = new QLabel;
m_pending_texture_label->setWordWrap(true);
layout->addWidget(m_pending_texture_label);
auto* refresh_layout = new QHBoxLayout;
m_auto_refresh_check = new QCheckBox(tr("Auto Refresh"));
m_auto_refresh_check->setToolTip(
tr("Continuously refresh the current matches list while this window is open.\n"
"Disable this when the window becomes sluggish and use Refresh manually."));
m_refresh_button = new QPushButton(tr("Refresh"));
refresh_layout->addWidget(m_auto_refresh_check);
refresh_layout->addStretch();
refresh_layout->addWidget(m_refresh_button);
layout->addLayout(refresh_layout);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
m_save_button = buttons->addButton(tr("Save To Elements Group Override"),
QDialogButtonBox::ActionRole);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
layout->addWidget(buttons);
}
void ElementsHuntingDialog::ConnectWidgets()
{
auto& manager = ElementsGroupManager::GetInstance();
connect(m_enable_check, &QCheckBox::toggled, this, [&manager](bool checked) {
manager.SetHuntEnabled(checked);
});
connect(m_option_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
[&manager, this]() {
manager.SetHuntingOption(static_cast<ShaderHunter::HuntingOption>(
m_option_combo->currentData().toInt()));
});
connect(m_match_filter_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
[&manager, this]() {
manager.SetSelectedMatchFilterExcluded(m_match_filter_mode_combo->currentData().toBool());
RequestRefresh();
});
connect(m_texture_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this]() { RefreshPendingTextureSummary(); });
connect(m_refresh_button, &QPushButton::clicked, this, &ElementsHuntingDialog::RequestRefresh);
connect(m_auto_refresh_check, &QCheckBox::toggled, this, [this](bool checked) {
if (checked)
RequestRefresh();
});
connect(m_seed_list, &QListWidget::itemSelectionChanged, this,
&ElementsHuntingDialog::OnSeedSelectionChanged);
connect(m_view_textures_button, &QPushButton::clicked, this,
&ElementsHuntingDialog::ShowTextureBrowser);
const auto apply_groups = [this, &manager]() {
manager.SetSeedGroupMask(m_projection_check->isChecked(), m_layer_check->isChecked(),
m_viewport_check->isChecked(), m_scissor_check->isChecked(),
m_render_state_check->isChecked());
RequestRefresh();
};
for (QCheckBox* checkbox : {m_projection_check, m_layer_check, m_viewport_check,
m_scissor_check, m_render_state_check})
{
connect(checkbox, &QCheckBox::toggled, this, apply_groups);
}
connect(m_prev_match_button, &QPushButton::clicked, this,
[this, &manager]() {
manager.PrevMatch();
RequestRefresh();
});
connect(m_next_match_button, &QPushButton::clicked, this,
[this, &manager]() {
manager.NextMatch();
RequestRefresh();
});
connect(m_current_match_list, &QListWidget::itemSelectionChanged, this,
&ElementsHuntingDialog::OnCurrentMatchSelectionChanged);
connect(m_current_match_list, &QListWidget::itemChanged, this,
&ElementsHuntingDialog::OnCurrentMatchItemChanged);
connect(m_save_button, &QPushButton::clicked, this, &ElementsHuntingDialog::SaveCurrentOverride);
}
void ElementsHuntingDialog::UpdateDisplay()
{
auto& manager = ElementsGroupManager::GetInstance();
const auto status = manager.GetStatus();
const bool refresh_lists = m_refresh_pending || (m_auto_refresh_check && m_auto_refresh_check->isChecked());
{
const QSignalBlocker blocker(m_enable_check);
m_enable_check->setChecked(status.hunt_enabled);
}
{
const QSignalBlocker blocker(m_option_combo);
const int idx = m_option_combo->findData(static_cast<int>(status.option));
if (idx >= 0)
m_option_combo->setCurrentIndex(idx);
}
{
const QSignalBlocker blocker(m_match_filter_mode_combo);
const int idx = m_match_filter_mode_combo->findData(status.selected_match_filter_excluded);
if (idx >= 0)
m_match_filter_mode_combo->setCurrentIndex(idx);
}
const auto candidates = manager.GetSeedCandidates();
if (refresh_lists)
{
{
const QSignalBlocker blocker(m_seed_list);
if (m_seed_list->count() != static_cast<int>(candidates.size()))
{
m_seed_list->clear();
for (size_t i = 0; i < candidates.size(); ++i)
{
auto* item = new QListWidgetItem(FormatSeedCandidateLabel(candidates[i], status.seed_signature));
item->setData(Qt::UserRole, static_cast<int>(i));
m_seed_list->addItem(item);
}
}
else
{
for (int i = 0; i < m_seed_list->count(); ++i)
m_seed_list->item(i)->setText(
FormatSeedCandidateLabel(candidates[static_cast<size_t>(i)], status.seed_signature));
}
if (status.selected_seed_index >= 0 && status.selected_seed_index < m_seed_list->count())
m_seed_list->setCurrentRow(status.selected_seed_index);
else
m_seed_list->clearSelection();
}
const auto current_matches = manager.GetCurrentMatches();
{
const QSignalBlocker blocker(m_current_match_list);
if (m_current_match_list->count() != static_cast<int>(current_matches.size()))
{
m_current_match_list->clear();
for (size_t i = 0; i < current_matches.size(); ++i)
{
auto* item = new QListWidgetItem(FormatCurrentMatchLabel(current_matches[i]));
item->setData(Qt::UserRole, static_cast<int>(i));
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(manager.IsCurrentMatchFilterEnabled(static_cast<int>(i)) ? Qt::Checked :
Qt::Unchecked);
if (!current_matches[i].active_this_frame)
item->setForeground(palette().color(QPalette::Disabled, QPalette::Text));
m_current_match_list->addItem(item);
}
}
else
{
for (int i = 0; i < m_current_match_list->count(); ++i)
{
auto* item = m_current_match_list->item(i);
item->setText(FormatCurrentMatchLabel(current_matches[static_cast<size_t>(i)]));
item->setCheckState(manager.IsCurrentMatchFilterEnabled(i) ? Qt::Checked : Qt::Unchecked);
item->setForeground(current_matches[static_cast<size_t>(i)].active_this_frame ?
palette().color(QPalette::Text) :
palette().color(QPalette::Disabled, QPalette::Text));
}
}
if (status.selected_match >= 0 && status.selected_match < m_current_match_list->count())
m_current_match_list->setCurrentRow(status.selected_match);
else
m_current_match_list->clearSelection();
}
m_refresh_pending = false;
}
if (status.seed_valid)
{
QStringList groups;
if (status.seed_signature.use_projection)
groups << tr("Projection");
if (status.seed_signature.use_layer)
groups << tr("Layer");
if (status.seed_signature.use_viewport)
groups << tr("Viewport");
if (status.seed_signature.use_scissor)
groups << tr("Scissor");
if (status.seed_signature.use_render_state)
groups << tr("Render State");
QString seed_summary = tr("Seed: %1\nGroups: %2\nChecked Match Filters: %3")
.arg(status.selected_seed_index >= 0 &&
status.selected_seed_index < static_cast<int>(candidates.size()) ?
FormatSeedSummary(candidates[static_cast<size_t>(status.selected_seed_index)],
status.seed_signature) :
tr("selected"))
.arg(groups.isEmpty() ? tr("(none)") :
groups.join(QStringLiteral(", ")))
.arg(status.selected_match_filter_count);
if (status.selected_match_filter_count > 0)
{
seed_summary += tr("\nMatch Mode: %1")
.arg(status.selected_match_filter_excluded ? tr("Exclude") :
tr("Include"));
}
m_seed_label->setText(seed_summary);
}
else
{
m_seed_label->setText(tr("Seed: (none)"));
}
{
const QSignalBlocker p(m_projection_check);
const QSignalBlocker l(m_layer_check);
const QSignalBlocker v(m_viewport_check);
const QSignalBlocker s(m_scissor_check);
const QSignalBlocker r(m_render_state_check);
m_projection_check->setChecked(status.seed_signature.use_projection);
m_layer_check->setChecked(status.seed_signature.use_layer);
m_viewport_check->setChecked(status.seed_signature.use_viewport);
m_scissor_check->setChecked(status.seed_signature.use_scissor);
m_render_state_check->setChecked(status.seed_signature.use_render_state);
}
if (status.highlighted_draw)
{
m_match_label->setText(tr("Matches: %1 / %2 (Active: %3)\nCurrent Match: %4\nPreview Scope: %5")
.arg(status.total_matches > 0 ? status.selected_match + 1 : 0)
.arg(status.total_matches)
.arg(status.active_matches)
.arg(tr("%1 | Covers %2 draw(s)")
.arg(FormatMatchLabel(*status.highlighted_draw))
.arg(status.highlighted_match_raw_draw_count))
.arg(GetPreviewScopeLabel(status)));
}
else
{
m_match_label->setText(tr("Matches: %1 / %2 (Active: %3)\nPreview Scope: %4")
.arg(status.total_matches > 0 ? status.selected_match + 1 : 0)
.arg(status.total_matches)
.arg(status.active_matches)
.arg(GetPreviewScopeLabel(status)));
}
}
void ElementsHuntingDialog::RequestRefresh()
{
m_refresh_pending = true;
UpdateDisplay();
}
void ElementsHuntingDialog::RefreshPendingTextureSummary()
{
if (m_pending_texture_hashes.empty())
{
m_pending_texture_label->setText(tr("Pending Texture Filters: (none)"));
return;
}
QStringList hashes;
const int shown = std::min<int>(3, static_cast<int>(m_pending_texture_hashes.size()));
for (int i = 0; i < shown; ++i)
{
hashes << QStringLiteral("%1")
.arg(static_cast<qulonglong>(m_pending_texture_hashes[static_cast<size_t>(i)]), 16,
16, QLatin1Char('0'));
}
if (static_cast<int>(m_pending_texture_hashes.size()) > shown)
{
hashes << tr("+%1 more").arg(static_cast<int>(m_pending_texture_hashes.size()) - shown);
}
m_pending_texture_label->setText(
tr("Pending Texture %1 Filter(s): %2")
.arg(m_texture_mode_combo->currentData().toBool() ? tr("Exclude") : tr("Include"))
.arg(hashes.join(QStringLiteral(", "))));
}
void ElementsHuntingDialog::OnSeedSelectionChanged()
{
const auto items = m_seed_list->selectedItems();
if (items.empty())
return;
const int index = items[0]->data(Qt::UserRole).toInt();
ElementsGroupManager::GetInstance().SelectSeedCandidate(index);
m_pending_texture_hashes.clear();
RefreshPendingTextureSummary();
RequestRefresh();
}
void ElementsHuntingDialog::ShowTextureBrowser()
{
const auto status = ElementsGroupManager::GetInstance().GetStatus();
if (!status.seed_valid)
{
QMessageBox::warning(this, tr("View Textures"),
tr("No active Elements Hunting preview scope is available."));
return;
}
TextureHashBrowserConfig browser_config;
browser_config.title = tr("Textures for Current Element Selection");
browser_config.empty_info_text =
tr("No textures were captured yet for the current preview scope.");
browser_config.current_label = tr("current preview scope");
browser_config.fetch_current_label = []() {
const auto current_status = ElementsGroupManager::GetInstance().GetStatus();
return current_status.selected_match_filter_count > 0 ?
(current_status.selected_match_filter_excluded ? QObject::tr("unchecked matches only") :
QObject::tr("checked matches only")) :
QObject::tr("whole current group");
};
browser_config.initial_selected_hashes = m_pending_texture_hashes;
browser_config.fetch_current_entries = []() {
return CollectTextureBrowserEntries(
ElementsGroupManager::GetInstance().GetCurrentTextureSourceDraws());
};
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
m_pending_texture_hashes = hashes;
RefreshPendingTextureSummary();
};
ShowTextureHashBrowserDialog(this, browser_config);
}
void ElementsHuntingDialog::SaveCurrentOverride()
{
if (m_game_id.empty())
{
QMessageBox::warning(this, tr("Elements Hunting"), tr("No game is currently running."));
return;
}
auto& manager = ElementsGroupManager::GetInstance();
const auto status = manager.GetStatus();
if (!status.highlighted_draw)
{
QMessageBox::warning(this, tr("Elements Hunting"),
tr("No highlighted match is available in the current frame."));
return;
}
if (!status.seed_valid)
{
QMessageBox::warning(this, tr("Elements Hunting"), tr("Seed a draw first."));
return;
}
ElementsGroupManager::ElementGroupOverride initial;
initial.name =
fmt::format("Element Draw {}", status.highlighted_draw->draw_index >= 0 ?
status.highlighted_draw->draw_index + 1 :
0);
initial.handling = ShaderHunter::HandlingType::Skip;
initial.runtime_element = status.seed_signature;
initial.texture_hashes = m_pending_texture_hashes;
initial.texture_hashes_excluded =
!m_pending_texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
initial.selected_match_filter = manager.GetSelectedMatchFilters();
initial.selected_match_filter_excluded = manager.GetSelectedMatchFilterExcluded();
initial.refinement_enabled = false;
ElementsGroupOverrideAddEditDialog dialog(this, &initial, CollectAvailableFlags(m_game_id));
if (dialog.exec() != QDialog::Accepted)
return;
auto overrides = ElementsGroupManager::LoadOverridesFromINI(m_game_id);
overrides.push_back(dialog.GetResult());
ElementsGroupManager::SaveOverridesToINI(m_game_id, overrides);
manager.LoadOverrides(m_game_id);
emit OverridesChanged();
}
void ElementsHuntingDialog::OnCurrentMatchSelectionChanged()
{
const auto items = m_current_match_list->selectedItems();
if (items.empty())
return;
ElementsGroupManager::GetInstance().SelectMatch(items[0]->data(Qt::UserRole).toInt());
RequestRefresh();
}
void ElementsHuntingDialog::OnCurrentMatchItemChanged(QListWidgetItem* item)
{
if (!item)
return;
ElementsGroupManager::GetInstance().SetCurrentMatchFilterEnabled(
item->data(Qt::UserRole).toInt(), item->checkState() == Qt::Checked);
RequestRefresh();
}
void ElementsHuntingDialog::closeEvent(QCloseEvent* event)
{
auto& manager = ElementsGroupManager::GetInstance();
manager.SetHuntEnabled(false);
manager.SetPopupOpen(false);
QDialog::closeEvent(event);
}
@@ -0,0 +1,68 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
class QCheckBox;
class QComboBox;
class QLabel;
class QListWidget;
class QListWidgetItem;
class QPushButton;
class QTimer;
class ElementsHuntingDialog : public QDialog
{
Q_OBJECT
public:
explicit ElementsHuntingDialog(std::string game_id, QWidget* parent = nullptr);
~ElementsHuntingDialog() override;
signals:
void OverridesChanged();
private:
void closeEvent(QCloseEvent* event) override;
void CreateWidgets();
void ConnectWidgets();
void UpdateDisplay();
void RequestRefresh();
void RefreshPendingTextureSummary();
void SaveCurrentOverride();
void OnSeedSelectionChanged();
void OnCurrentMatchSelectionChanged();
void OnCurrentMatchItemChanged(QListWidgetItem* item);
void ShowTextureBrowser();
std::string m_game_id;
QCheckBox* m_enable_check = nullptr;
QComboBox* m_option_combo = nullptr;
QComboBox* m_match_filter_mode_combo = nullptr;
QComboBox* m_texture_mode_combo = nullptr;
QListWidget* m_seed_list = nullptr;
QLabel* m_seed_label = nullptr;
QLabel* m_match_label = nullptr;
QLabel* m_pending_texture_label = nullptr;
QCheckBox* m_auto_refresh_check = nullptr;
QCheckBox* m_projection_check = nullptr;
QCheckBox* m_layer_check = nullptr;
QCheckBox* m_viewport_check = nullptr;
QCheckBox* m_scissor_check = nullptr;
QCheckBox* m_render_state_check = nullptr;
QPushButton* m_refresh_button = nullptr;
QPushButton* m_prev_match_button = nullptr;
QPushButton* m_next_match_button = nullptr;
QPushButton* m_view_textures_button = nullptr;
QListWidget* m_current_match_list = nullptr;
QPushButton* m_save_button = nullptr;
QTimer* m_update_timer = nullptr;
bool m_refresh_pending = true;
std::vector<u64> m_pending_texture_hashes;
};
@@ -46,6 +46,8 @@ EnhancementsWidget::EnhancementsWidget(GraphicsPane* gfx_pane)
connect(gfx_pane, &GraphicsPane::UseGPUTextureDecodingChanged, this, [this] {
m_arbitrary_mipmap_detection->setEnabled(!ReadSetting(Config::GFX_ENABLE_GPU_TEXTURE_DECODING));
});
connect(&Settings::Instance(), &Settings::ConfigChanged, this,
&EnhancementsWidget::UpdateStereoscopyAvailability);
}
constexpr int ANISO_1x = std::to_underlying(AnisotropicFilteringMode::Force1x);
@@ -208,9 +210,9 @@ void EnhancementsWidget::CreateWidgets()
++row;
// Stereoscopy
auto* stereoscopy_box = new QGroupBox(tr("Stereoscopy"));
m_stereoscopy_box = new QGroupBox(tr("Stereoscopy"));
auto* stereoscopy_layout = new QGridLayout();
stereoscopy_box->setLayout(stereoscopy_layout);
m_stereoscopy_box->setLayout(stereoscopy_layout);
m_3d_mode = new ConfigChoice({tr("Off"), tr("Side-by-Side"), tr("Top-and-Bottom"), tr("Anaglyph"),
tr("HDMI 3D"), tr("Passive")},
@@ -246,7 +248,7 @@ void EnhancementsWidget::CreateWidgets()
m_3d_per_eye_resolution->hide();
main_layout->addWidget(enhancements_box);
main_layout->addWidget(stereoscopy_box);
main_layout->addWidget(m_stereoscopy_box);
main_layout->addStretch();
setLayout(main_layout);
@@ -351,12 +353,7 @@ void EnhancementsWidget::OnBackendChanged()
m_configure_color_correction->setEnabled(g_backend_info.bSupportsPostProcessing);
m_hdr->setEnabled(g_backend_info.bSupportsHDROutput);
// Stereoscopy
const bool supports_stereoscopy = g_backend_info.bSupportsGeometryShaders;
m_3d_mode->setEnabled(supports_stereoscopy);
m_3d_convergence->setEnabled(supports_stereoscopy);
m_3d_depth->setEnabled(supports_stereoscopy);
m_3d_swap_eyes->setEnabled(supports_stereoscopy);
UpdateStereoscopyAvailability();
// PostProcessing
const bool supports_postprocessing = g_backend_info.bSupportsPostProcessing;
@@ -378,6 +375,13 @@ void EnhancementsWidget::OnBackendChanged()
UpdateAntialiasingOptions();
}
void EnhancementsWidget::UpdateStereoscopyAvailability()
{
const bool supports_stereoscopy = g_backend_info.bSupportsGeometryShaders;
const bool openxr_enabled = Config::Get(Config::GFX_VR_ENABLE_OPENXR);
m_stereoscopy_box->setEnabled(supports_stereoscopy && !openxr_enabled);
}
void EnhancementsWidget::ShaderChanged()
{
auto shader = ReadSetting(Config::GFX_ENHANCE_POST_SHADER);
@@ -412,6 +416,7 @@ void EnhancementsWidget::OnConfigChanged()
// being global.
m_texture_filtering_combo->setEnabled(ReadSetting(Config::GFX_HACK_FAST_TEXTURE_SAMPLING));
m_arbitrary_mipmap_detection->setEnabled(!ReadSetting(Config::GFX_ENABLE_GPU_TEXTURE_DECODING));
UpdateStereoscopyAvailability();
UpdateAntialiasingOptions();
// Needs to update after deleting a key for 3d settings.
@@ -539,7 +544,8 @@ void EnhancementsWidget::AddDescriptions()
"emulation speed and sometimes causes issues.<br><br>Side-by-Side and Top-and-Bottom are "
"used by most 3D TVs.<br>Anaglyph is used for Red-Cyan colored glasses.<br>HDMI 3D is "
"used when the monitor supports 3D display resolutions.<br>Passive is another type of 3D "
"used by some TVs.<br><br><dolphin_emphasis>If unsure, select Off.</dolphin_emphasis>");
"used by some TVs."
"<br><br><dolphin_emphasis>If unsure, select Off.</dolphin_emphasis>");
static const char TR_3D_DEPTH_DESCRIPTION[] = QT_TR_NOOP(
"Controls the separation distance between the virtual cameras.<br><br>A higher "
"value creates a stronger feeling of depth while a lower value is more comfortable.");
@@ -11,6 +11,7 @@ class ConfigComplexChoice;
class ConfigStringChoice;
class ConfigFloatSlider;
class GraphicsPane;
class QGroupBox;
class QPushButton;
class QLabel;
class ToolTipPushButton;
@@ -37,6 +38,7 @@ private:
void AddDescriptions();
void OnBackendChanged();
void UpdateStereoscopyAvailability();
void UpdateAntialiasingOptions();
void LoadPostProcessingShaders();
void ShaderChanged();
@@ -63,6 +65,7 @@ private:
ConfigBool* m_hdr;
// Stereoscopy
QGroupBox* m_stereoscopy_box;
ConfigChoice* m_3d_mode;
ConfigFloatSlider* m_3d_depth;
QLabel* m_3d_depth_value;
@@ -32,6 +32,19 @@
#include "VideoCommon/VideoBackendBase.h"
#include "VideoCommon/VideoConfig.h"
namespace
{
QString GetBackendDisplayNameForUI(std::string_view backend_name)
{
if (backend_name == "Direct3D 11")
return QObject::tr("Direct3D 11 (VR)");
if (backend_name == "Vulkan")
return QObject::tr("Vulkan (VR)");
return QObject::tr(backend_name.data());
}
} // namespace
GeneralWidget::GeneralWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetConfigLayer()}
{
CreateWidgets();
@@ -56,7 +69,7 @@ void GeneralWidget::CreateWidgets()
std::vector<std::pair<QString, QString>> options;
for (auto& backend : VideoBackendBase::GetAvailableBackends())
{
options.push_back(std::make_pair(tr(backend->GetDisplayName().c_str()),
options.push_back(std::make_pair(GetBackendDisplayNameForUI(backend->GetDisplayName()),
QString::fromStdString(backend->GetConfigName())));
}
m_backend_combo = new ConfigStringChoice(options, Config::MAIN_GFX_BACKEND, m_game_layer);
@@ -292,7 +305,7 @@ void GeneralWidget::AddDescriptions()
m_backend_combo->SetTitle(tr("Backend"));
m_backend_combo->SetDescription(
tr(TR_BACKEND_DESCRIPTION)
.arg(QString::fromStdString(VideoBackendBase::GetDefaultBackendDisplayName())));
.arg(GetBackendDisplayNameForUI(VideoBackendBase::GetDefaultBackendDisplayName())));
m_adapter_combo->SetTitle(tr("Adapter"));
@@ -16,6 +16,7 @@
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
#include "DolphinQt/Config/GameConfigWidget.h"
#include "DolphinQt/Config/Graphics/GraphicsPane.h"
#include "DolphinQt/Settings.h"
#include "VideoCommon/VideoConfig.h"
@@ -26,6 +27,28 @@ HacksWidget::HacksWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetCon
AddDescriptions();
connect(gfx_pane, &GraphicsPane::BackendChanged, this, &HacksWidget::OnBackendChanged);
const auto update_openxr_immediate_xfb = [this] {
// When VR Lock Head Pose is active, Immediately Present XFB must stay off.
const bool vr_lock = Config::Get(Config::GFX_VR_ENABLE_OPENXR) &&
Config::Get(Config::GFX_VR_LOCK_HEAD_POSE);
// When VR Force Immediately Present XFB is active, force it on and grey out.
const bool vr_force = Config::Get(Config::GFX_VR_ENABLE_OPENXR) &&
Config::Get(Config::GFX_VR_AUTO_IMMEDIATE_XFB);
m_immediate_xfb->setDisabled(vr_lock || vr_force);
if (vr_lock)
{
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, false);
}
else if (vr_force && !Config::Get(Config::GFX_HACK_IMMEDIATE_XFB))
{
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, true);
}
UpdateSkipPresentingDuplicateFramesEnabled();
};
connect(&Settings::Instance(), &Settings::ConfigChanged, this, update_openxr_immediate_xfb);
update_openxr_immediate_xfb();
OnBackendChanged(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND)));
connect(m_gpu_texture_decoding, &QCheckBox::toggled,
[gfx_pane] { emit gfx_pane->UseGPUTextureDecodingChanged(); });
@@ -0,0 +1,419 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/HideObjectAddEditDialog.h"
#include <algorithm>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSlider>
#include <QVBoxLayout>
#include "VideoCommon/HideObjectEngine.h"
HideObjectAddEditDialog::HideObjectAddEditDialog(
QWidget* parent, const HideObjectEngine::HideObject* existing_code,
const std::vector<HideObjectEngine::HideObject>& all_codes)
: QDialog(parent), m_all_codes(all_codes)
{
if (existing_code)
{
m_is_edit = true;
m_original_name = existing_code->name;
m_result = *existing_code;
if (!m_result.entries.empty())
m_current_entry = m_result.entries[0];
}
else
{
m_current_entry.type = HideObjectEngine::HideObjectType::Bits16;
m_current_entry.value_upper = 0;
m_current_entry.value_lower = 0;
}
setWindowTitle(m_is_edit ? tr("Edit Hide Object Code") : tr("Add Hide Object Code"));
setMinimumWidth(400);
CreateWidgets();
ConnectWidgets();
UpdateValueDisplay();
}
void HideObjectAddEditDialog::CreateWidgets()
{
auto* name_label = new QLabel(tr("Name:"));
m_name_edit = new QLineEdit;
if (m_is_edit)
m_name_edit->setText(QString::fromStdString(m_result.name));
else
m_name_edit->setPlaceholderText(tr("Enter code name..."));
auto* type_label = new QLabel(tr("Size:"));
m_type_combo = new QComboBox;
for (int i = 0; i < static_cast<int>(HideObjectEngine::HideObjectType::Count); i++)
m_type_combo->addItem(QString::fromLatin1(
HideObjectEngine::GetTypeName(static_cast<HideObjectEngine::HideObjectType>(i))));
m_type_combo->setCurrentIndex(static_cast<int>(m_current_entry.type));
auto* value_label = new QLabel(tr("Value (hex):"));
m_value_edit = new QLineEdit;
m_value_edit->setFont(QFont(QStringLiteral("Courier New"), 10));
m_up_button = new QPushButton(tr("Up"));
m_down_button = new QPushButton(tr("Down"));
m_range_finder_toggle = new QCheckBox(tr("Range Finder"));
m_range_label = new QLabel;
m_range_lower_slider = new QSlider(Qt::Horizontal);
m_range_upper_slider = new QSlider(Qt::Horizontal);
m_range_lower_slider->setRange(0, 255);
m_range_upper_slider->setRange(0, 255);
m_range_lower_slider->setValue(0);
m_range_upper_slider->setValue(255);
UpdateRangeLabel();
const QString tooltip =
tr("The Up/Down buttons can be used to find new codes.\n"
"While the game is playing, find an object you want to hide and select '8bits'.\n"
"Keep clicking Up until the object disappears.\n"
"Now choose '16bits' and continue clicking Up until the object is hidden again.\n"
"Repeat until the code is long enough to be unique.\n"
"Warning: Too short of a code may hide other objects too.");
m_up_button->setToolTip(tooltip);
m_down_button->setToolTip(tooltip);
auto* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto* grid = new QGridLayout;
grid->addWidget(name_label, 0, 0);
grid->addWidget(m_name_edit, 0, 1, 1, 3);
grid->addWidget(type_label, 1, 0);
grid->addWidget(m_type_combo, 1, 1, 1, 3);
grid->addWidget(value_label, 2, 0);
grid->addWidget(m_value_edit, 2, 1);
grid->addWidget(m_up_button, 2, 2);
grid->addWidget(m_down_button, 2, 3);
grid->addWidget(m_range_finder_toggle, 3, 0, 1, 4);
grid->addWidget(m_range_label, 4, 0, 1, 4);
grid->addWidget(new QLabel(tr("Lower Bound:")), 5, 0);
grid->addWidget(m_range_lower_slider, 5, 1, 1, 3);
grid->addWidget(new QLabel(tr("Upper Bound:")), 6, 0);
grid->addWidget(m_range_upper_slider, 6, 1, 1, 3);
m_range_label->setEnabled(false);
m_range_lower_slider->setEnabled(false);
m_range_upper_slider->setEnabled(false);
auto* layout = new QVBoxLayout{this};
layout->addLayout(grid);
layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, this, &HideObjectAddEditDialog::OnAccept);
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
void HideObjectAddEditDialog::ConnectWidgets()
{
connect(m_type_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&HideObjectAddEditDialog::OnTypeChanged);
connect(m_up_button, &QPushButton::clicked, this, &HideObjectAddEditDialog::OnUpClicked);
connect(m_down_button, &QPushButton::clicked, this, &HideObjectAddEditDialog::OnDownClicked);
connect(m_range_finder_toggle, &QCheckBox::toggled, this,
&HideObjectAddEditDialog::OnRangeFinderToggled);
connect(m_range_lower_slider, &QSlider::valueChanged, this,
&HideObjectAddEditDialog::OnRangeBoundsChanged);
connect(m_range_upper_slider, &QSlider::valueChanged, this,
&HideObjectAddEditDialog::OnRangeBoundsChanged);
}
void HideObjectAddEditDialog::UpdateValueDisplay()
{
const int char_len = HideObjectEngine::GetByteCount(m_current_entry.type) * 2;
if (char_len <= 16)
{
// Fits in value_lower
m_value_edit->setText(
QStringLiteral("%1").arg(m_current_entry.value_lower, char_len, 16, QLatin1Char('0')).toUpper());
}
else
{
// Need both upper and lower
const int upper_chars = char_len - 16;
m_value_edit->setText(
QStringLiteral("%1%2")
.arg(m_current_entry.value_upper, upper_chars, 16, QLatin1Char('0'))
.arg(m_current_entry.value_lower, 16, 16, QLatin1Char('0'))
.toUpper());
}
}
bool HideObjectAddEditDialog::ParseValueFromUI()
{
const QString text = m_value_edit->text().trimmed();
if (text.isEmpty())
{
QMessageBox::warning(this, tr("Error"), tr("Value cannot be empty."));
return false;
}
const int expected_chars = HideObjectEngine::GetByteCount(m_current_entry.type) * 2;
bool ok = false;
if (expected_chars <= 16)
{
m_current_entry.value_lower = text.toULongLong(&ok, 16);
m_current_entry.value_upper = 0;
}
else
{
// Split: upper portion + lower 16 hex chars
if (text.length() > 16)
{
const QString upper_str = text.left(text.length() - 16);
const QString lower_str = text.right(16);
m_current_entry.value_upper = upper_str.toULongLong(&ok, 16);
if (ok)
m_current_entry.value_lower = lower_str.toULongLong(&ok, 16);
}
else
{
m_current_entry.value_upper = 0;
m_current_entry.value_lower = text.toULongLong(&ok, 16);
}
}
if (!ok)
{
QMessageBox::warning(this, tr("Error"),
tr("Invalid hex value. Use characters 0-9 and A-F only."));
return false;
}
return true;
}
void HideObjectAddEditDialog::OnTypeChanged()
{
if (!ParseValueFromUI())
return;
const int old_byte_count = HideObjectEngine::GetByteCount(m_current_entry.type);
m_current_entry.type = static_cast<HideObjectEngine::HideObjectType>(m_type_combo->currentIndex());
const int new_byte_count = HideObjectEngine::GetByteCount(m_current_entry.type);
auto shift_left_128 = [](u64& upper, u64& lower, int bits) {
if (bits <= 0)
return;
if (bits >= 128)
{
upper = 0;
lower = 0;
return;
}
if (bits >= 64)
{
upper = lower << (bits - 64);
lower = 0;
return;
}
upper = (upper << bits) | (lower >> (64 - bits));
lower <<= bits;
};
auto shift_right_128 = [](u64& upper, u64& lower, int bits) {
if (bits <= 0)
return;
if (bits >= 128)
{
upper = 0;
lower = 0;
return;
}
if (bits >= 64)
{
lower = upper >> (bits - 64);
upper = 0;
return;
}
lower = (lower >> bits) | (upper << (64 - bits));
upper >>= bits;
};
// Keep current bytes in place relative to the most-significant side when size changes.
// Example: 8-bit 01 -> 16-bit 0100; and 16-bit 0100 -> 8-bit 01.
const int shift_bytes = new_byte_count - old_byte_count;
if (shift_bytes > 0)
shift_left_128(m_current_entry.value_upper, m_current_entry.value_lower, shift_bytes * 8);
else if (shift_bytes < 0)
shift_right_128(m_current_entry.value_upper, m_current_entry.value_lower, -shift_bytes * 8);
// Truncate to the newly selected size.
if (new_byte_count <= 8)
{
m_current_entry.value_upper = 0;
if (new_byte_count < 8)
{
const u64 mask = (1ULL << (new_byte_count * 8)) - 1;
m_current_entry.value_lower &= mask;
}
}
else
{
const int upper_bytes = new_byte_count - 8;
if (upper_bytes < 8)
{
const u64 mask = (1ULL << (upper_bytes * 8)) - 1;
m_current_entry.value_upper &= mask;
}
}
UpdateValueDisplay();
ApplyTemporarily();
}
void HideObjectAddEditDialog::OnUpClicked()
{
if (!ParseValueFromUI())
return;
// Brute-force only the last byte (two hex chars), without carrying into higher bytes.
const u8 low_byte = static_cast<u8>(m_current_entry.value_lower & 0xFFULL);
const u8 next_low_byte = static_cast<u8>(low_byte + 1);
m_current_entry.value_lower = (m_current_entry.value_lower & ~0xFFULL) | next_low_byte;
UpdateValueDisplay();
ApplyTemporarily();
}
void HideObjectAddEditDialog::OnDownClicked()
{
if (!ParseValueFromUI())
return;
// Brute-force only the last byte (two hex chars), without borrowing from higher bytes.
const u8 low_byte = static_cast<u8>(m_current_entry.value_lower & 0xFFULL);
const u8 prev_low_byte = static_cast<u8>(low_byte - 1);
m_current_entry.value_lower = (m_current_entry.value_lower & ~0xFFULL) | prev_low_byte;
UpdateValueDisplay();
ApplyTemporarily();
}
void HideObjectAddEditDialog::OnRangeFinderToggled(bool enabled)
{
m_range_label->setEnabled(enabled);
m_range_lower_slider->setEnabled(enabled);
m_range_upper_slider->setEnabled(enabled);
UpdateRangeLabel();
ApplyTemporarily();
}
void HideObjectAddEditDialog::OnRangeBoundsChanged()
{
if (sender() == m_range_lower_slider && m_range_lower_slider->value() > m_range_upper_slider->value())
m_range_upper_slider->setValue(m_range_lower_slider->value());
else if (sender() == m_range_upper_slider &&
m_range_upper_slider->value() < m_range_lower_slider->value())
m_range_lower_slider->setValue(m_range_upper_slider->value());
UpdateRangeLabel();
if (!m_range_finder_toggle->isChecked())
return;
if (!ParseValueFromUI())
return;
ApplyTemporarily();
}
void HideObjectAddEditDialog::UpdateRangeLabel()
{
const int lower = std::min(m_range_lower_slider->value(), m_range_upper_slider->value());
const int upper = std::max(m_range_lower_slider->value(), m_range_upper_slider->value());
m_range_label->setText(
tr("Current range: %1 - %2")
.arg(QStringLiteral("%1").arg(lower, 2, 16, QLatin1Char('0')).toUpper())
.arg(QStringLiteral("%1").arg(upper, 2, 16, QLatin1Char('0')).toUpper()));
}
void HideObjectAddEditDialog::ApplyTemporarily()
{
// Temporarily apply this single code to the running game for immediate visual feedback.
std::vector<HideObjectEngine::HideObject> temp_list;
if (m_range_finder_toggle->isChecked())
{
// Range finder: apply all values for the last byte in [lower..upper], fixed upper bytes.
const int lower = std::min(m_range_lower_slider->value(), m_range_upper_slider->value());
const int upper = std::max(m_range_lower_slider->value(), m_range_upper_slider->value());
temp_list.reserve(static_cast<size_t>(upper - lower + 1));
for (int v = lower; v <= upper; ++v)
{
HideObjectEngine::HideObject temp_code;
temp_code.name = "temp_range";
temp_code.active = true;
temp_code.user_defined = false;
HideObjectEngine::HideObjectEntry entry = m_current_entry;
entry.value_lower = (entry.value_lower & ~0xFFULL) | static_cast<u64>(v & 0xFF);
temp_code.entries.push_back(entry);
temp_list.push_back(std::move(temp_code));
}
}
else
{
// Single-value brute force: only the current entry.
HideObjectEngine::HideObject temp_code;
temp_code.name = "temp_brute_force";
temp_code.entries.push_back(m_current_entry);
temp_code.active = true;
temp_code.user_defined = false;
temp_list.push_back(std::move(temp_code));
}
HideObjectEngine::Engine::GetInstance().ApplyCodes(temp_list);
}
void HideObjectAddEditDialog::OnAccept()
{
const QString name = m_name_edit->text().trimmed();
if (name.isEmpty())
{
QMessageBox::warning(this, tr("Error"), tr("Please enter a name for this code."));
return;
}
// Check name uniqueness
for (const auto& code : m_all_codes)
{
if (code.name == name.toStdString() && code.name != m_original_name)
{
QMessageBox::warning(this, tr("Error"),
tr("Name is already in use. Please choose a unique name."));
return;
}
}
if (!ParseValueFromUI())
return;
m_result.name = name.toStdString();
m_result.entries.clear();
m_result.entries.push_back(m_current_entry);
m_result.active = true;
m_result.user_defined = true;
accept();
}
@@ -0,0 +1,62 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include "VideoCommon/HideObjectEngine.h"
class QComboBox;
class QCheckBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QSlider;
class HideObjectAddEditDialog : public QDialog
{
Q_OBJECT
public:
// Pass nullptr for new code, or pointer to existing code for editing.
// parent_codes is the full list (for name uniqueness check).
explicit HideObjectAddEditDialog(QWidget* parent,
const HideObjectEngine::HideObject* existing_code,
const std::vector<HideObjectEngine::HideObject>& all_codes);
HideObjectEngine::HideObject GetResult() const { return m_result; }
private:
void CreateWidgets();
void ConnectWidgets();
void OnTypeChanged();
void OnUpClicked();
void OnDownClicked();
void OnRangeFinderToggled(bool enabled);
void OnRangeBoundsChanged();
void OnAccept();
void UpdateValueDisplay();
void UpdateRangeLabel();
bool ParseValueFromUI();
void ApplyTemporarily();
bool m_is_edit = false;
std::string m_original_name; // For edit mode: original name to exclude from uniqueness check
HideObjectEngine::HideObject m_result;
HideObjectEngine::HideObjectEntry m_current_entry;
const std::vector<HideObjectEngine::HideObject>& m_all_codes;
QLineEdit* m_name_edit;
QComboBox* m_type_combo;
QLineEdit* m_value_edit;
QPushButton* m_up_button;
QPushButton* m_down_button;
QCheckBox* m_range_finder_toggle;
QLabel* m_range_label;
QSlider* m_range_lower_slider;
QSlider* m_range_upper_slider;
};
@@ -0,0 +1,227 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/HideObjectWidget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include "DolphinQt/Config/HideObjectAddEditDialog.h"
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
#include "VideoCommon/HideObjectEngine.h"
HideObjectWidget::HideObjectWidget(std::string game_id) : m_game_id(std::move(game_id))
{
CreateWidgets();
ConnectWidgets();
LoadCodes();
}
HideObjectWidget::~HideObjectWidget() = default;
void HideObjectWidget::CreateWidgets()
{
m_code_list = new QListWidget;
m_code_add = new NonDefaultQPushButton(tr("&Add..."));
m_code_edit = new NonDefaultQPushButton(tr("&Edit..."));
m_code_remove = new NonDefaultQPushButton(tr("&Remove"));
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
m_code_reload = new NonDefaultQPushButton(tr("Reload &Codes"));
m_code_edit->setEnabled(false);
m_code_remove->setEnabled(false);
auto* info_label = new QLabel(
tr("Hide object codes suppress rendering of specific vertex data patterns.\n"
"Use Add to create a new code, or use the Up/Down buttons in the editor\n"
"to discover codes while the game is running."));
info_label->setWordWrap(true);
auto* button_layout = new QHBoxLayout;
button_layout->addWidget(m_code_add);
button_layout->addWidget(m_code_edit);
button_layout->addWidget(m_code_remove);
button_layout->addStretch();
button_layout->addWidget(m_code_refresh);
button_layout->addWidget(m_code_reload);
auto* layout = new QVBoxLayout{this};
layout->addWidget(info_label);
layout->addWidget(m_code_list);
layout->addLayout(button_layout);
}
void HideObjectWidget::ConnectWidgets()
{
connect(m_code_list, &QListWidget::itemChanged, this, &HideObjectWidget::OnItemChanged);
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
&HideObjectWidget::OnSelectionChanged);
connect(m_code_add, &QPushButton::clicked, this, &HideObjectWidget::OnAddClicked);
connect(m_code_edit, &QPushButton::clicked, this, &HideObjectWidget::OnEditClicked);
connect(m_code_remove, &QPushButton::clicked, this, &HideObjectWidget::OnRemoveClicked);
connect(m_code_refresh, &QPushButton::clicked, this, &HideObjectWidget::OnRefreshClicked);
connect(m_code_reload, &QPushButton::clicked, this, &HideObjectWidget::OnReloadClicked);
}
void HideObjectWidget::LoadCodes()
{
m_codes = HideObjectEngine::LoadFromINI(m_game_id);
m_code_list->setEnabled(!m_game_id.empty());
m_code_edit->setEnabled(false);
m_code_remove->setEnabled(false);
UpdateList();
}
void HideObjectWidget::SaveCodes()
{
HideObjectEngine::SaveToINI(m_game_id, m_codes);
}
void HideObjectWidget::UpdateList()
{
const QSignalBlocker blocker(m_code_list);
m_code_list->clear();
for (size_t i = 0; i < m_codes.size(); i++)
{
const auto& code = m_codes[i];
QString display;
if (!code.entries.empty())
{
const auto& entry = code.entries[0];
const int char_len = HideObjectEngine::GetByteCount(entry.type) * 2;
QString value_str;
if (char_len <= 16)
{
value_str = QStringLiteral("%1")
.arg(entry.value_lower, char_len, 16, QLatin1Char('0'))
.toUpper();
}
else
{
const int upper_chars = char_len - 16;
value_str = QStringLiteral("%1%2")
.arg(entry.value_upper, upper_chars, 16, QLatin1Char('0'))
.arg(entry.value_lower, 16, 16, QLatin1Char('0'))
.toUpper();
}
display = QStringLiteral("%1 [%2 %3]")
.arg(QString::fromStdString(code.name))
.arg(QString::fromLatin1(HideObjectEngine::GetTypeName(entry.type)))
.arg(value_str);
}
else
{
display = QString::fromStdString(code.name);
}
auto* item = new QListWidgetItem(display);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
item->setCheckState(code.active ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, static_cast<int>(i));
m_code_list->addItem(item);
}
}
void HideObjectWidget::OnItemChanged(QListWidgetItem* item)
{
const int row = m_code_list->row(item);
if (row < 0 || row >= static_cast<int>(m_codes.size()))
return;
m_codes[row].active = (item->checkState() == Qt::Checked);
SaveCodes();
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
}
void HideObjectWidget::OnSelectionChanged()
{
const auto items = m_code_list->selectedItems();
const bool has_selection = !items.empty();
m_code_edit->setEnabled(has_selection);
m_code_remove->setEnabled(has_selection);
}
void HideObjectWidget::OnAddClicked()
{
HideObjectAddEditDialog dialog(this, nullptr, m_codes);
if (dialog.exec() == QDialog::Accepted)
{
m_codes.push_back(dialog.GetResult());
SaveCodes();
UpdateList();
// Re-apply codes to runtime engine so the new code takes effect immediately
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
}
else
{
// Dialog was cancelled — restore the full code set to the runtime engine
// (in case brute-force Up/Down applied temporary codes)
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
}
}
void HideObjectWidget::OnEditClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int row = m_code_list->row(items[0]);
if (row < 0 || row >= static_cast<int>(m_codes.size()))
return;
HideObjectAddEditDialog dialog(this, &m_codes[row], m_codes);
if (dialog.exec() == QDialog::Accepted)
{
m_codes[row] = dialog.GetResult();
SaveCodes();
UpdateList();
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
}
else
{
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
}
}
void HideObjectWidget::OnRemoveClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int row = m_code_list->row(items[0]);
if (row < 0 || row >= static_cast<int>(m_codes.size()))
return;
m_codes.erase(m_codes.begin() + row);
SaveCodes();
UpdateList();
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
m_code_edit->setEnabled(false);
m_code_remove->setEnabled(false);
}
void HideObjectWidget::OnRefreshClicked()
{
LoadCodes();
}
void HideObjectWidget::OnReloadClicked()
{
HideObjectEngine::Engine::GetInstance().LoadCodes(m_game_id);
}
@@ -0,0 +1,49 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
#include <string>
#include <vector>
#include "VideoCommon/HideObjectEngine.h"
class QListWidget;
class QListWidgetItem;
class QPushButton;
class HideObjectWidget : public QWidget
{
Q_OBJECT
public:
explicit HideObjectWidget(std::string game_id);
~HideObjectWidget() override;
private:
void CreateWidgets();
void ConnectWidgets();
void UpdateList();
void LoadCodes();
void SaveCodes();
void OnItemChanged(QListWidgetItem* item);
void OnSelectionChanged();
void OnAddClicked();
void OnEditClicked();
void OnRemoveClicked();
void OnRefreshClicked();
void OnReloadClicked();
std::string m_game_id;
QListWidget* m_code_list;
QPushButton* m_code_add;
QPushButton* m_code_edit;
QPushButton* m_code_remove;
QPushButton* m_code_refresh;
QPushButton* m_code_reload;
std::vector<HideObjectEngine::HideObject> m_codes;
};
@@ -0,0 +1,328 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ImportShaderFindElementDialog.h"
#include <algorithm>
#include <QAbstractItemView>
#include <QBrush>
#include <QCheckBox>
#include <QCloseEvent>
#include <QColor>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QSignalBlocker>
#include <QTimer>
#include <QVBoxLayout>
namespace
{
QString FormatTextureSummary(const std::array<u64, 8>& textures)
{
QStringList parts;
for (u64 texture_hash : textures)
{
if (texture_hash == 0)
continue;
parts << QStringLiteral("%1")
.arg(static_cast<qulonglong>(texture_hash), 16, 16, QLatin1Char('0'));
if (parts.size() >= 2)
break;
}
return parts.isEmpty() ? QObject::tr("No textures") : parts.join(QStringLiteral(", "));
}
QString FormatProjectionLabel(const ShaderHunter::RuntimeElementSignature& sig)
{
if (sig.perspective)
{
return QObject::tr("Perspective %1x%2")
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
}
return QObject::tr("Ortho L%1").arg(sig.ortho_layer);
}
bool RuntimeElementSignaturesEqual(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;
}
QString FormatCandidateLabel(const ImportShaderFindElementDialog::CandidateRow& row)
{
const auto& sig = row.candidate.signature;
const auto& draw = row.candidate.representative_draw;
return QObject::tr("%1 | VP %2,%3 %4x%5 | SC %6,%7 %8,%9 | Draw #%10 | Score %11 | %12\n"
"Stage: VS %13 | PS %14 | GS %15 | Tex %16")
.arg(FormatProjectionLabel(sig))
.arg(sig.viewport_x)
.arg(sig.viewport_y)
.arg(sig.viewport_width)
.arg(sig.viewport_height)
.arg(sig.scissor_left)
.arg(sig.scissor_top)
.arg(sig.scissor_right)
.arg(sig.scissor_bottom)
.arg(draw.draw_index + 1)
.arg(row.score)
.arg(row.reason_summary)
.arg(static_cast<qulonglong>(draw.vs_hash), 16, 16, QLatin1Char('0'))
.arg(static_cast<qulonglong>(draw.ps_hash), 16, 16, QLatin1Char('0'))
.arg(static_cast<qulonglong>(draw.gs_hash), 16, 16, QLatin1Char('0'))
.arg(FormatTextureSummary(draw.textures));
}
} // namespace
ImportShaderFindElementDialog::ImportShaderFindElementDialog(
const std::string& game_id, const ShaderHunter::ShaderOverride& source_shader, QWidget* parent)
: QDialog(parent), m_game_id(game_id), m_source_shader(source_shader)
{
setWindowTitle(tr("Import Shader -> Find Element"));
setMinimumWidth(620);
auto& manager = ElementsGroupManager::GetInstance();
manager.SetPopupOpen(true);
if (!m_game_id.empty())
manager.LoadOverridesIfNeeded(m_game_id);
CreateWidgets();
ConnectWidgets();
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, &ImportShaderFindElementDialog::UpdateDisplay);
m_update_timer->start(100);
UpdateDisplay();
}
ImportShaderFindElementDialog::~ImportShaderFindElementDialog() = default;
void ImportShaderFindElementDialog::CreateWidgets()
{
auto* layout = new QVBoxLayout(this);
const QString shader_name = QString::fromStdString(m_source_shader.name);
m_info_label = new QLabel(
tr("Find a live runtime element seed candidate that corresponds to the saved shader '%1'.\n"
"Candidates are scored using the shader anchor and copied texture filters.")
.arg(shader_name));
m_info_label->setWordWrap(true);
layout->addWidget(m_info_label);
m_matching_only_check = new QCheckBox(tr("Show Matching Candidates Only"));
m_matching_only_check->setChecked(true);
layout->addWidget(m_matching_only_check);
m_candidate_list = new QListWidget;
m_candidate_list->setSelectionMode(QAbstractItemView::SingleSelection);
layout->addWidget(m_candidate_list);
m_status_label = new QLabel(tr("Waiting for live seed candidates..."));
m_status_label->setWordWrap(true);
layout->addWidget(m_status_label);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel);
m_open_hunting_button = buttons->addButton(tr("Open Elements Hunting"),
QDialogButtonBox::ActionRole);
m_use_candidate_button =
buttons->addButton(tr("Use Selected Candidate"), QDialogButtonBox::AcceptRole);
m_use_candidate_button->setEnabled(false);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_use_candidate_button, &QPushButton::clicked, this, &QDialog::accept);
}
void ImportShaderFindElementDialog::ConnectWidgets()
{
connect(m_matching_only_check, &QCheckBox::toggled, this,
&ImportShaderFindElementDialog::UpdateDisplay);
connect(m_candidate_list, &QListWidget::itemSelectionChanged, this,
&ImportShaderFindElementDialog::OnSelectionChanged);
connect(m_open_hunting_button, &QPushButton::clicked, this, [this]() {
m_open_elements_hunting = true;
reject();
});
}
std::optional<ElementsGroupManager::SeedCandidate> ImportShaderFindElementDialog::GetSelectedCandidate() const
{
const auto items = m_candidate_list->selectedItems();
if (items.empty())
return std::nullopt;
const int row = items[0]->data(Qt::UserRole).toInt();
if (row < 0 || row >= static_cast<int>(m_rows.size()))
return std::nullopt;
return m_rows[static_cast<size_t>(row)].candidate;
}
bool ImportShaderFindElementDialog::ShouldOpenElementsHunting() const
{
return m_open_elements_hunting;
}
void ImportShaderFindElementDialog::closeEvent(QCloseEvent* event)
{
ElementsGroupManager::GetInstance().SetPopupOpen(false);
QDialog::closeEvent(event);
}
ImportShaderFindElementDialog::CandidateRow ImportShaderFindElementDialog::BuildCandidateRow(
const ElementsGroupManager::SeedCandidate& candidate) const
{
CandidateRow row;
row.candidate = candidate;
const auto& draw = candidate.representative_draw;
const u64 draw_hash = draw.GetHash(m_source_shader.type);
const u64 draw_family = draw.GetFamily(m_source_shader.type);
if (m_source_shader.hash_family_match && m_source_shader.family_signature != 0)
{
row.shader_match = (draw_family != 0 && draw_family == m_source_shader.family_signature);
row.reason_summary = row.shader_match ? tr("shader family match") : tr("shader family mismatch");
}
else
{
row.shader_match = (draw_hash != 0 && draw_hash == m_source_shader.hash);
row.reason_summary = row.shader_match ? tr("shader exact match") : tr("shader exact mismatch");
}
row.texture_match = true;
if (!m_source_shader.texture_hashes.empty())
{
bool any_texture_match = false;
for (u64 texture_hash : draw.textures)
{
if (texture_hash == 0)
continue;
if (std::find(m_source_shader.texture_hashes.begin(), m_source_shader.texture_hashes.end(),
texture_hash) != m_source_shader.texture_hashes.end())
{
any_texture_match = true;
break;
}
}
row.texture_match =
m_source_shader.texture_hashes_excluded ? !any_texture_match : any_texture_match;
row.reason_summary += row.texture_match ?
(m_source_shader.texture_hashes_excluded ?
tr(", excluded textures clear") :
tr(", texture match")) :
(m_source_shader.texture_hashes_excluded ?
tr(", excluded texture present") :
tr(", texture mismatch"));
}
row.full_match = row.shader_match && row.texture_match;
row.score = 0;
if (row.shader_match)
row.score += 100;
if (row.texture_match)
row.score += m_source_shader.texture_hashes.empty() ? 0 : 25;
row.score += std::min(candidate.occurrence_count, 9);
return row;
}
void ImportShaderFindElementDialog::UpdateDisplay()
{
std::optional<ShaderHunter::RuntimeElementSignature> selected_signature;
if (const auto selected = GetSelectedCandidate(); selected.has_value())
selected_signature = selected->signature;
std::vector<CandidateRow> rows;
rows.reserve(ElementsGroupManager::GetInstance().GetSeedCandidates().size());
for (const auto& candidate : ElementsGroupManager::GetInstance().GetSeedCandidates())
{
CandidateRow row = BuildCandidateRow(candidate);
if (!m_matching_only_check->isChecked() || row.full_match)
rows.push_back(std::move(row));
}
std::sort(rows.begin(), rows.end(), [](const CandidateRow& lhs, const CandidateRow& rhs) {
if (lhs.full_match != rhs.full_match)
return lhs.full_match > rhs.full_match;
if (lhs.score != rhs.score)
return lhs.score > rhs.score;
return lhs.candidate.representative_draw.draw_index < rhs.candidate.representative_draw.draw_index;
});
m_rows = rows;
{
const QSignalBlocker blocker(m_candidate_list);
m_candidate_list->clear();
int row_to_select = -1;
for (size_t i = 0; i < m_rows.size(); ++i)
{
auto* item = new QListWidgetItem(FormatCandidateLabel(m_rows[i]));
item->setData(Qt::UserRole, static_cast<int>(i));
if (!m_rows[i].full_match)
item->setForeground(QBrush(QColor(150, 150, 150)));
m_candidate_list->addItem(item);
if (selected_signature.has_value() &&
RuntimeElementSignaturesEqual(m_rows[i].candidate.signature, *selected_signature))
{
row_to_select = static_cast<int>(i);
}
}
if (row_to_select >= 0)
m_candidate_list->setCurrentRow(row_to_select);
else if (!m_rows.empty())
m_candidate_list->setCurrentRow(0);
}
if (m_rows.empty())
{
m_status_label->setText(
tr("No live seed candidates currently match this shader import.\n"
"Keep the game rendering this scene, disable 'Show Matching Candidates Only', or open "
"Elements Hunting for manual inspection."));
}
else
{
const int matching_count = static_cast<int>(std::count_if(
m_rows.begin(), m_rows.end(), [](const CandidateRow& row) { return row.full_match; }));
m_status_label->setText(
tr("Candidates: %1 total, %2 matching.\nSelect one candidate to use as the runtime "
"element seed.")
.arg(m_rows.size())
.arg(matching_count));
}
OnSelectionChanged();
}
void ImportShaderFindElementDialog::OnSelectionChanged()
{
m_use_candidate_button->setEnabled(!m_candidate_list->selectedItems().empty());
}
@@ -0,0 +1,64 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <QString>
#include <optional>
#include <string>
#include <vector>
#include "VideoCommon/ElementsGroupManager.h"
#include "VideoCommon/ShaderHunter.h"
class QCheckBox;
class QLabel;
class QListWidget;
class QListWidgetItem;
class QPushButton;
class QTimer;
class ImportShaderFindElementDialog : public QDialog
{
Q_OBJECT
public:
struct CandidateRow
{
ElementsGroupManager::SeedCandidate candidate;
bool shader_match = false;
bool texture_match = false;
bool full_match = false;
int score = 0;
QString reason_summary;
};
explicit ImportShaderFindElementDialog(const std::string& game_id,
const ShaderHunter::ShaderOverride& source_shader,
QWidget* parent = nullptr);
~ImportShaderFindElementDialog() override;
std::optional<ElementsGroupManager::SeedCandidate> GetSelectedCandidate() const;
bool ShouldOpenElementsHunting() const;
private:
void closeEvent(QCloseEvent* event) override;
void CreateWidgets();
void ConnectWidgets();
void UpdateDisplay();
void OnSelectionChanged();
CandidateRow BuildCandidateRow(const ElementsGroupManager::SeedCandidate& candidate) const;
std::string m_game_id;
ShaderHunter::ShaderOverride m_source_shader;
QLabel* m_info_label = nullptr;
QCheckBox* m_matching_only_check = nullptr;
QListWidget* m_candidate_list = nullptr;
QLabel* m_status_label = nullptr;
QPushButton* m_open_hunting_button = nullptr;
QPushButton* m_use_candidate_button = nullptr;
QTimer* m_update_timer = nullptr;
bool m_open_elements_hunting = false;
std::vector<CandidateRow> m_rows;
};
@@ -0,0 +1,40 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/Mapping/HotkeyVR.h"
#include <QGroupBox>
#include <QHBoxLayout>
#include "Core/HotkeyManager.h"
HotkeyVR::HotkeyVR(MappingWindow* window) : MappingWidget(window)
{
CreateMainLayout();
}
void HotkeyVR::CreateMainLayout()
{
m_main_layout = new QHBoxLayout();
m_main_layout->addWidget(CreateGroupBox(tr("VR"), HotkeyManagerEmu::GetHotkeyGroup(HKGP_VR)));
m_main_layout->addWidget(
CreateGroupBox(tr("Shader"), HotkeyManagerEmu::GetHotkeyGroup(HKGP_VR_SHADER)));
setLayout(m_main_layout);
}
InputConfig* HotkeyVR::GetConfig()
{
return HotkeyManagerEmu::GetConfig();
}
void HotkeyVR::LoadSettings()
{
HotkeyManagerEmu::LoadConfig();
}
void HotkeyVR::SaveSettings()
{
HotkeyManagerEmu::GetConfig()->SaveConfig();
}
@@ -0,0 +1,25 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "DolphinQt/Config/Mapping/MappingWidget.h"
class QHBoxLayout;
class HotkeyVR final : public MappingWidget
{
Q_OBJECT
public:
explicit HotkeyVR(MappingWindow* window);
InputConfig* GetConfig() override;
private:
void LoadSettings() override;
void SaveSettings() override;
void CreateMainLayout();
QHBoxLayout* m_main_layout;
};
@@ -17,7 +17,9 @@
#include "Common/MathUtil.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/Camera.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
@@ -61,6 +63,26 @@ QPen GetInputDotPen(QPen pen)
return GetCosmeticPen(pen);
}
std::optional<Wiimote::OpenXRWiiRemoteState>
GetOpenXRWiiRemoteStateForGroup(int wiimote_port, const std::string& group_name,
bool prefer_left_hand = false)
{
if (prefer_left_hand)
return Wiimote::GetOpenXRHandState(true);
if (wiimote_port < WIIMOTE_CHAN_0 || wiimote_port >= MAX_WIIMOTES)
return std::nullopt;
if (group_name != WiimoteEmu::Wiimote::IR_GROUP &&
group_name != WiimoteEmu::Wiimote::ACCELEROMETER_GROUP &&
group_name != WiimoteEmu::Wiimote::GYROSCOPE_GROUP)
{
return std::nullopt;
}
return Wiimote::GetOpenXRWiiRemoteState(static_cast<unsigned int>(wiimote_port));
}
} // namespace
QPen MappingIndicator::GetBBoxPen() const
@@ -327,6 +349,18 @@ QColor CursorIndicator::GetGateBrushColor() const
void CursorIndicator::Draw()
{
if (const auto openxr_state =
GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_cursor_group.name))
{
const auto adj_coord =
openxr_state->ir_visible
? std::make_optional(ControllerEmu::ReshapableInput::ReshapeData{openxr_state->ir_x,
openxr_state->ir_y})
: std::nullopt;
DrawReshapableInput(m_cursor_group, adj_coord);
return;
}
const auto adj_coord = m_cursor_group.GetState(true);
DrawReshapableInput(m_cursor_group,
@@ -707,7 +741,13 @@ void ShakeMappingIndicator::Draw()
void AccelerometerMappingIndicator::Draw()
{
const auto accel_state = m_accel_group.GetState();
auto accel_state = m_accel_group.GetState();
if (const auto openxr_state =
GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_accel_group.name, m_prefer_left_hand))
{
accel_state = Common::Vec3{openxr_state->acceleration[0], openxr_state->acceleration[1],
openxr_state->acceleration[2]};
}
const auto state = accel_state.value_or(Common::Vec3{});
QPainter p(this);
@@ -783,7 +823,12 @@ void AccelerometerMappingIndicator::Draw()
void GyroMappingIndicator::Update(float elapsed_seconds)
{
const auto gyro_state = m_gyro_group.GetState();
auto gyro_state = m_gyro_group.GetState();
if (const auto openxr_state = GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_gyro_group.name))
{
gyro_state = Common::Vec3{openxr_state->angular_velocity[0], openxr_state->angular_velocity[1],
openxr_state->angular_velocity[2]};
}
const auto angular_velocity = gyro_state.value_or(Common::Vec3{});
m_state *= WiimoteEmu::GetRotationFromGyroscope(angular_velocity * Common::Vec3(-1, +1, -1) *
elapsed_seconds);
@@ -805,8 +850,16 @@ void GyroMappingIndicator::Update(float elapsed_seconds)
void GyroMappingIndicator::Draw()
{
const auto gyro_state = m_gyro_group.GetState();
const auto raw_gyro_state = m_gyro_group.GetRawState();
auto gyro_state = m_gyro_group.GetState();
auto raw_gyro_state = m_gyro_group.GetRawState();
if (const auto openxr_state = GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_gyro_group.name))
{
const Common::Vec3 openxr_angular_velocity{
openxr_state->angular_velocity[0], openxr_state->angular_velocity[1],
openxr_state->angular_velocity[2]};
gyro_state = openxr_angular_velocity;
raw_gyro_state = openxr_angular_velocity;
}
const auto angular_velocity = gyro_state.value_or(Common::Vec3{});
const auto jitter = raw_gyro_state - m_previous_velocity;
m_previous_velocity = raw_gyro_state;
@@ -134,7 +134,10 @@ private:
class CursorIndicator : public ReshapableInputIndicator
{
public:
explicit CursorIndicator(ControllerEmu::Cursor& cursor) : m_cursor_group(cursor) {}
explicit CursorIndicator(ControllerEmu::Cursor& cursor, int wiimote_port = -1)
: m_cursor_group(cursor), m_wiimote_port(wiimote_port)
{
}
QColor GetGateBrushColor() const final;
@@ -142,6 +145,7 @@ private:
void Draw() override;
ControllerEmu::Cursor& m_cursor_group;
int m_wiimote_port = -1;
};
class MixedTriggersIndicator : public MappingIndicator
@@ -197,8 +201,11 @@ private:
class AccelerometerMappingIndicator : public SquareIndicator
{
public:
explicit AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer& accel)
: m_accel_group(accel)
explicit AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer& accel,
int wiimote_port = -1,
bool prefer_left_hand = false)
: m_accel_group(accel), m_wiimote_port(wiimote_port),
m_prefer_left_hand(prefer_left_hand)
{
}
@@ -206,18 +213,24 @@ private:
void Draw() override;
ControllerEmu::IMUAccelerometer& m_accel_group;
int m_wiimote_port = -1;
bool m_prefer_left_hand = false;
};
class GyroMappingIndicator : public SquareIndicator
{
public:
explicit GyroMappingIndicator(ControllerEmu::IMUGyroscope& gyro) : m_gyro_group(gyro) {}
explicit GyroMappingIndicator(ControllerEmu::IMUGyroscope& gyro, int wiimote_port = -1)
: m_gyro_group(gyro), m_wiimote_port(wiimote_port)
{
}
private:
void Draw() override;
void Update(float elapsed_seconds) override;
ControllerEmu::IMUGyroscope& m_gyro_group;
int m_wiimote_port = -1;
Common::Quaternion m_state = Common::Quaternion::Identity();
Common::Vec3 m_previous_velocity = {};
float m_stable_time = 0;
@@ -19,6 +19,11 @@
#include "DolphinQt/Config/Mapping/MappingNumeric.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
#include "Core/Config/WiimoteSettings.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
@@ -58,6 +63,19 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
MappingIndicator* indicator = nullptr;
const bool is_openxr_wiimote_source =
GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES &&
Config::Get(Config::GetInfoForWiimoteSource(GetPort())) == WiimoteSource::OpenXR;
const bool is_openxr_wiimote_point_group =
is_openxr_wiimote_source &&
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Point);
const bool is_openxr_wiimote_imu_accel_group =
is_openxr_wiimote_source &&
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer);
const bool is_openxr_wiimote_imu_gyro_group =
is_openxr_wiimote_source &&
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope);
switch (group->type)
{
case ControllerEmu::GroupType::Shake:
@@ -73,7 +91,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
break;
case ControllerEmu::GroupType::Cursor:
indicator = new CursorIndicator(*static_cast<ControllerEmu::Cursor*>(group));
indicator = new CursorIndicator(*static_cast<ControllerEmu::Cursor*>(group), GetPort());
break;
case ControllerEmu::GroupType::Force:
@@ -81,13 +99,49 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
break;
case ControllerEmu::GroupType::IMUAccelerometer:
indicator =
new AccelerometerMappingIndicator(*static_cast<ControllerEmu::IMUAccelerometer*>(group));
{
int indicator_port = -1;
bool prefer_left_hand = false;
if (GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES)
{
auto* const wiimote_group = Wiimote::GetWiimoteGroup(
GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer);
if (group == wiimote_group)
{
indicator_port = GetPort();
}
else
{
auto* const nunchuk_group =
Wiimote::GetNunchukGroup(GetPort(), WiimoteEmu::NunchukGroup::IMUAccelerometer);
if (group == nunchuk_group)
{
indicator_port = GetPort();
prefer_left_hand = true;
}
}
}
indicator = new AccelerometerMappingIndicator(
*static_cast<ControllerEmu::IMUAccelerometer*>(group), indicator_port, prefer_left_hand);
break;
}
case ControllerEmu::GroupType::IMUGyroscope:
indicator = new GyroMappingIndicator(*static_cast<ControllerEmu::IMUGyroscope*>(group));
{
int indicator_port = -1;
if (GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES)
{
auto* const wiimote_group =
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope);
if (group == wiimote_group)
indicator_port = GetPort();
}
indicator = new GyroMappingIndicator(*static_cast<ControllerEmu::IMUGyroscope*>(group),
indicator_port);
break;
}
case ControllerEmu::GroupType::Stick:
indicator = new AnalogStickIndicator(*static_cast<ControllerEmu::ReshapableInput*>(group));
@@ -144,10 +198,11 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
// "Calibrate" button.
if (indicator != nullptr)
{
const bool need_calibration = group->type == ControllerEmu::GroupType::Cursor ||
const bool need_calibration = !is_openxr_wiimote_point_group &&
(group->type == ControllerEmu::GroupType::Cursor ||
group->type == ControllerEmu::GroupType::Stick ||
group->type == ControllerEmu::GroupType::Tilt ||
group->type == ControllerEmu::GroupType::Force;
group->type == ControllerEmu::GroupType::Force);
if (need_calibration)
{
@@ -159,8 +214,16 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
}
}
for (auto& control : group->controls)
for (size_t i = 0; i < group->controls.size(); ++i)
{
if (is_openxr_wiimote_point_group && i < 4)
continue;
if (is_openxr_wiimote_imu_accel_group || is_openxr_wiimote_imu_gyro_group)
continue;
auto& control = group->controls[i];
CreateControl(control.get(), form_layout, !indicator);
}
AddSettingWidgets(form_layout, group, ControllerEmu::SettingVisibility::Normal);
@@ -176,7 +239,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
[this, group] { ShowAdvancedControlGroupDialog(group); });
}
if (group->type == ControllerEmu::GroupType::Cursor)
if (group->type == ControllerEmu::GroupType::Cursor && !is_openxr_wiimote_point_group)
{
QPushButton* mouse_button = new QPushButton(tr("Use Mouse Controlled Pointing"));
form_layout->insertRow(2, mouse_button);
@@ -28,6 +28,8 @@
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
#include "Core/Config/WiimoteSettings.h"
#include "Core/HW/Wiimote.h"
#include "DolphinQt/Config/Mapping/FreeLookGeneral.h"
#include "DolphinQt/Config/Mapping/FreeLookRotation.h"
@@ -45,8 +47,10 @@
#include "DolphinQt/Config/Mapping/HotkeyStatesOther.h"
#include "DolphinQt/Config/Mapping/HotkeyTAS.h"
#include "DolphinQt/Config/Mapping/HotkeyUSBEmu.h"
#include "DolphinQt/Config/Mapping/HotkeyVR.h"
#include "DolphinQt/Config/Mapping/HotkeyWii.h"
#include "DolphinQt/Config/Mapping/MappingCommon.h"
#include "DolphinQt/Config/Mapping/OpenXRWiimoteConfigSessionController.h"
#include "DolphinQt/Config/Mapping/WiimoteEmuExtension.h"
#include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionInput.h"
#include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionSimulation.h"
@@ -91,6 +95,9 @@ MappingWindow::MappingWindow(QWidget* parent, Type type, int port_num)
const auto lock = GetController()->GetStateLock();
emit ConfigChanged();
if (m_mapping_type == Type::MAPPING_WIIMOTE_EMU && m_is_openxr_wiimote)
m_openxr_config_session_controller = new OpenXRWiimoteConfigSessionController(this, m_port);
auto* filter = new WindowActivationEventFilter(this);
installEventFilter(filter);
@@ -442,6 +449,7 @@ void MappingWindow::UpdateDeviceList()
void MappingWindow::SetMappingType(MappingWindow::Type type)
{
m_mapping_type = type;
MappingWidget* widget;
switch (type)
@@ -472,17 +480,20 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
break;
case Type::MAPPING_WIIMOTE_EMU:
{
m_is_openxr_wiimote =
Config::Get(Config::GetInfoForWiimoteSource(GetPort())) == WiimoteSource::OpenXR;
auto* extension = new WiimoteEmuExtension(this);
auto* extension_motion_input = new WiimoteEmuExtensionMotionInput(this);
auto* extension_motion_simulation = new WiimoteEmuExtensionMotionSimulation(this);
widget = new WiimoteEmuGeneral(this, extension);
setWindowTitle(tr("Wii Remote %1").arg(GetPort() + 1));
AddWidget(tr("General and Options"), widget);
AddWidget(tr("Motion Simulation"), new WiimoteEmuMotionControl(this));
if (!m_is_openxr_wiimote)
AddWidget(tr("Motion Simulation"), new WiimoteEmuMotionControl(this));
AddWidget(tr("Motion Input"), new WiimoteEmuMotionControlIMU(this));
AddWidget(tr("Extension"), extension);
m_extension_motion_simulation_tab =
AddWidget(EXTENSION_MOTION_SIMULATION_TAB_NAME, extension_motion_simulation);
m_extension_tab = AddWidget(tr("Extension"), extension);
m_extension_motion_simulation_tab = AddWidget(EXTENSION_MOTION_SIMULATION_TAB_NAME,
extension_motion_simulation);
m_extension_motion_input_tab =
AddWidget(EXTENSION_MOTION_INPUT_TAB_NAME, extension_motion_input);
// Hide tabs by default. "Nunchuk" selection triggers an event to show them.
@@ -502,6 +513,7 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
AddWidget(tr("Wii and Wii Remote"), new HotkeyWii(this));
AddWidget(tr("Controller Profile"), new HotkeyControllerProfile(this));
AddWidget(tr("Graphics"), new HotkeyGraphics(this));
AddWidget(tr("VR"), new HotkeyVR(this));
AddWidget(tr("USB Emulation"), new HotkeyUSBEmu(this));
// i18n: Stereoscopic 3D
AddWidget(tr("3D"), new Hotkey3D(this));
@@ -615,19 +627,28 @@ void MappingWindow::OnClearFieldsPressed()
void MappingWindow::ShowExtensionMotionTabs(bool show)
{
const int motion_sim_tab_index =
m_extension_motion_simulation_tab ? m_tab_widget->indexOf(m_extension_motion_simulation_tab) :
-1;
const int motion_input_tab_index = m_tab_widget->indexOf(m_extension_motion_input_tab);
if (show)
{
m_tab_widget->addTab(m_extension_motion_simulation_tab, EXTENSION_MOTION_SIMULATION_TAB_NAME);
m_tab_widget->addTab(m_extension_motion_input_tab, EXTENSION_MOTION_INPUT_TAB_NAME);
if (!m_is_openxr_wiimote && m_extension_motion_simulation_tab && motion_sim_tab_index == -1)
m_tab_widget->addTab(m_extension_motion_simulation_tab, EXTENSION_MOTION_SIMULATION_TAB_NAME);
if (motion_input_tab_index == -1)
m_tab_widget->addTab(m_extension_motion_input_tab, EXTENSION_MOTION_INPUT_TAB_NAME);
}
else
{
m_tab_widget->removeTab(5);
m_tab_widget->removeTab(4);
if (motion_sim_tab_index != -1)
m_tab_widget->removeTab(motion_sim_tab_index);
if (motion_input_tab_index != -1)
m_tab_widget->removeTab(motion_input_tab_index);
}
}
void MappingWindow::ActivateExtensionTab()
{
m_tab_widget->setCurrentIndex(3);
if (m_extension_tab)
m_tab_widget->setCurrentIndex(m_tab_widget->indexOf(m_extension_tab));
}
@@ -4,6 +4,7 @@
#pragma once
#include <QDialog>
#include <QPointer>
#include <QString>
namespace ControllerEmu
@@ -13,6 +14,7 @@ class EmulatedController;
class InputConfig;
class MappingButton;
class OpenXRWiimoteConfigSessionController;
class QComboBox;
class QDialogButtonBox;
@@ -129,12 +131,15 @@ private:
QPushButton* m_reset_clear;
QTabWidget* m_tab_widget;
QWidget* m_extension_tab = nullptr;
QWidget* m_extension_motion_input_tab;
QWidget* m_extension_motion_simulation_tab;
QWidget* m_extension_motion_simulation_tab = nullptr;
const QString EXTENSION_MOTION_INPUT_TAB_NAME = tr("Extension Motion Input");
const QString EXTENSION_MOTION_SIMULATION_TAB_NAME = tr("Extension Motion Simulation");
Type m_mapping_type;
const int m_port;
bool m_is_openxr_wiimote = false;
InputConfig* m_config;
QPointer<OpenXRWiimoteConfigSessionController> m_openxr_config_session_controller;
};
@@ -0,0 +1,237 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/Mapping/OpenXRWiimoteConfigSessionController.h"
#include <cstring>
#include <QFont>
#include <QFontMetrics>
#include <QImage>
#include <QPainter>
#include <QPen>
#include <QTimer>
#include <array>
#include <string>
#include <vector>
#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/WiimoteSettings.h"
#include "Core/Core.h"
#include "Core/HW/Wiimote.h"
#include "Core/System.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "DolphinQt/Settings.h"
#if defined(ENABLE_VR) && defined(_WIN32)
#include "Common/VR/OpenXRInputState.h"
#include "VideoCommon/VR/OpenXRManager.h"
#include "VideoCommon/VR/OpenXRUtilitySession.h"
#endif
namespace
{
constexpr int OVERLAY_WIDTH = 1024;
constexpr int OVERLAY_HEIGHT = 704;
#if defined(ENABLE_VR) && defined(_WIN32)
QString BoolToDigit(bool value)
{
return value ? QStringLiteral("1") : QStringLiteral("0");
}
QString BuildDigitalStateLine1(const Common::VR::OpenXRControllerState& controller)
{
return QObject::tr("Primary: %1 Secondary: %2 Menu: %3")
.arg(BoolToDigit(controller.primary_button), BoolToDigit(controller.secondary_button),
BoolToDigit(controller.menu_button));
}
QString BuildDigitalStateLine2(const Common::VR::OpenXRControllerState& controller)
{
return QObject::tr("Trigger Btn: %1 Grip Btn: %2 Stick Btn: %3")
.arg(BoolToDigit(controller.trigger_button), BoolToDigit(controller.squeeze_button),
BoolToDigit(controller.thumbstick_button));
}
QString BuildAnalogStateLine(const Common::VR::OpenXRControllerState& controller)
{
return QObject::tr("Trigger: %1 Grip: %2 Stick: (%3, %4)")
.arg(QString::number(controller.trigger_value, 'f', 2),
QString::number(controller.squeeze_value, 'f', 2),
QString::number(controller.thumbstick_x, 'f', 2),
QString::number(controller.thumbstick_y, 'f', 2));
}
std::vector<u8> RenderOverlayImage(int port, const Common::VR::OpenXRInputSnapshot& snapshot)
{
QImage image(OVERLAY_WIDTH, OVERLAY_HEIGHT, QImage::Format_RGBA8888);
image.fill(qRgba(0, 0, 0, 0));
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
const QRect background_rect(24, 24, OVERLAY_WIDTH - 48, OVERLAY_HEIGHT - 48);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(18, 22, 28, 220));
painter.drawRoundedRect(background_rect, 24, 24);
QFont title_font = painter.font();
title_font.setPointSize(28);
title_font.setBold(true);
painter.setFont(title_font);
painter.setPen(QColor(245, 247, 250));
painter.drawText(QRect(56, 54, OVERLAY_WIDTH - 112, 50), Qt::AlignLeft | Qt::AlignVCenter,
QObject::tr("OpenXR Wii Remote Binding"));
QFont subtitle_font = painter.font();
subtitle_font.setPointSize(18);
subtitle_font.setBold(false);
painter.setFont(subtitle_font);
painter.setPen(QColor(180, 188, 198));
painter.drawText(QRect(56, 114, OVERLAY_WIDTH - 112, 36), Qt::AlignLeft | Qt::AlignVCenter,
QObject::tr("Wii Remote %1").arg(port + 1));
painter.drawText(QRect(56, 164, OVERLAY_WIDTH - 112, 36), Qt::AlignLeft | Qt::AlignVCenter,
QObject::tr("Use the desktop window to bind controls"));
const std::array<QString, 2> labels = {QObject::tr("Left Controller"),
QObject::tr("Right Controller")};
QFont status_font = painter.font();
status_font.setPointSize(17);
painter.setFont(status_font);
for (int i = 0; i < 2; ++i)
{
const auto& controller = snapshot.controllers[i];
const int top = 236 + i * 168;
const QRect row_rect(56, top, OVERLAY_WIDTH - 112, 144);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(32, 38, 46, 210));
painter.drawRoundedRect(row_rect, 16, 16);
painter.setBrush(controller.connected ? QColor(44, 182, 125) : QColor(196, 84, 84));
painter.drawEllipse(QPoint(92, top + 28), 10, 10);
painter.setPen(QColor(238, 240, 243));
painter.drawText(QRect(120, top + 2, 280, 52), Qt::AlignLeft | Qt::AlignVCenter, labels[i]);
painter.setPen(controller.connected ? QColor(129, 228, 180) : QColor(246, 151, 151));
painter.drawText(QRect(OVERLAY_WIDTH - 300, top + 2, 220, 52),
Qt::AlignRight | Qt::AlignVCenter,
controller.connected ? QObject::tr("Connected") : QObject::tr("Waiting"));
QFont details_font = painter.font();
details_font.setPointSize(13);
painter.setFont(details_font);
painter.setPen(QColor(208, 214, 221));
painter.drawText(QRect(88, top + 52, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
BuildDigitalStateLine1(controller));
painter.drawText(QRect(88, top + 82, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
BuildDigitalStateLine2(controller));
painter.setPen(QColor(162, 171, 182));
painter.drawText(QRect(88, top + 112, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
BuildAnalogStateLine(controller));
painter.setFont(status_font);
}
painter.setPen(QColor(140, 148, 160));
painter.drawText(QRect(56, OVERLAY_HEIGHT - 60, OVERLAY_WIDTH - 112, 30),
Qt::AlignLeft | Qt::AlignVCenter,
snapshot.runtime_active ? QObject::tr("OpenXR runtime active")
: QObject::tr("Waiting for OpenXR runtime"));
painter.end();
std::vector<u8> pixels(static_cast<size_t>(image.sizeInBytes()));
std::memcpy(pixels.data(), image.constBits(), pixels.size());
return pixels;
}
#endif
} // namespace
OpenXRWiimoteConfigSessionController::OpenXRWiimoteConfigSessionController(MappingWindow* window,
int port)
: QObject(window), m_window(window), m_port(port), m_overlay_timer(new QTimer(this))
{
m_overlay_timer->setInterval(100);
connect(m_overlay_timer, &QTimer::timeout, this,
&OpenXRWiimoteConfigSessionController::UpdateOverlay);
connect(window, &QDialog::finished, this, &OpenXRWiimoteConfigSessionController::Stop);
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
[this](Core::State state) {
if (state != Core::State::Uninitialized)
Stop();
});
QTimer::singleShot(0, this, &OpenXRWiimoteConfigSessionController::TryStart);
}
OpenXRWiimoteConfigSessionController::~OpenXRWiimoteConfigSessionController()
{
Stop();
}
void OpenXRWiimoteConfigSessionController::TryStart()
{
#if defined(ENABLE_VR) && defined(_WIN32)
if (m_session)
return;
if (Core::GetState(Core::System::GetInstance()) != Core::State::Uninitialized)
return;
if (!Config::Get(Config::GFX_VR_ENABLE_OPENXR_CONFIG_SCENE))
return;
if (Config::Get(Config::GetInfoForWiimoteSource(m_port)) != WiimoteSource::OpenXR)
return;
if (VR::g_openxr)
return;
m_session = std::make_unique<VR::OpenXRUtilitySession>();
if (!m_session->Start())
{
m_session.reset();
return;
}
UpdateOverlay();
m_overlay_timer->start();
#endif
}
void OpenXRWiimoteConfigSessionController::Stop()
{
m_overlay_timer->stop();
#if defined(ENABLE_VR) && defined(_WIN32)
if (m_session)
{
m_session->Stop();
m_session.reset();
}
#endif
}
void OpenXRWiimoteConfigSessionController::UpdateOverlay()
{
#if defined(ENABLE_VR) && defined(_WIN32)
if (!m_session)
return;
if (!m_session->IsRunning())
{
Stop();
return;
}
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
m_session->SetOverlayImage(RenderOverlayImage(m_port, snapshot), OVERLAY_WIDTH, OVERLAY_HEIGHT);
#endif
}
@@ -0,0 +1,39 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <QObject>
namespace VR
{
#if defined(ENABLE_VR) && defined(_WIN32)
class OpenXRUtilitySession;
#endif
}
class MappingWindow;
class QTimer;
class OpenXRWiimoteConfigSessionController final : public QObject
{
Q_OBJECT
public:
OpenXRWiimoteConfigSessionController(MappingWindow* window, int port);
~OpenXRWiimoteConfigSessionController() override;
private:
void TryStart();
void Stop();
void UpdateOverlay();
MappingWindow* const m_window;
const int m_port;
QTimer* const m_overlay_timer;
#if defined(ENABLE_VR) && defined(_WIN32)
std::unique_ptr<VR::OpenXRUtilitySession> m_session;
#endif
};
@@ -8,6 +8,7 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "Core/Config/WiimoteSettings.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
@@ -22,14 +23,19 @@ void WiimoteEmuMotionControl::CreateMainLayout()
{
m_main_layout = new QHBoxLayout();
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Shake)));
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Point)));
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Tilt)));
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Swing)));
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(GetPort()));
if (source != WiimoteSource::OpenXR)
{
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Shake)));
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Tilt)));
m_main_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Swing)));
}
setLayout(m_main_layout);
}
@@ -3,14 +3,13 @@
#include "DolphinQt/Config/Mapping/WiimoteEmuMotionControlIMU.h"
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QString>
#include <QVBoxLayout>
#include "Core/Config/WiimoteSettings.h"
#include "Core/HW/Wiimote.h"
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
@@ -27,34 +26,54 @@ WiimoteEmuMotionControlIMU::WiimoteEmuMotionControlIMU(MappingWindow* window)
void WiimoteEmuMotionControlIMU::CreateMainLayout()
{
auto* warning_layout = new QHBoxLayout();
auto* warning_label =
new QLabel(tr("The controls under Accelerometer and Gyroscope are designed to "
"interface directly with motion sensor hardware. They are not intended for "
"mapping traditional buttons, triggers or axes. You might need to configure "
"alternate input sources before using these controls."));
warning_label->setWordWrap(true);
auto* warning_input_sources_button = new QPushButton(tr("Alternate Input Sources"));
warning_layout->addWidget(
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxWarning, warning_label), 1);
warning_layout->addWidget(warning_input_sources_button);
connect(warning_input_sources_button, &QPushButton::clicked, this, [this] {
ControllerInterfaceWindow window{this};
window.exec();
});
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(GetPort()));
auto* groups_layout = new QHBoxLayout();
groups_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUPoint)));
groups_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IRPassthrough)));
groups_layout->addWidget(CreateGroupBox(
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
groups_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(),
source == WiimoteSource::OpenXR ?
WiimoteEmu::WiimoteGroup::Point :
WiimoteEmu::WiimoteGroup::IMUPoint)));
if (source != WiimoteSource::OpenXR)
{
groups_layout->addWidget(CreateGroupBox(
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IRPassthrough)));
groups_layout->addWidget(CreateGroupBox(
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
groups_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
}
else
{
auto* imu_layout = new QVBoxLayout();
imu_layout->addWidget(
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
imu_layout->addWidget(CreateGroupBox(
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
groups_layout->addLayout(imu_layout);
}
m_main_layout = new QVBoxLayout();
m_main_layout->addLayout(warning_layout);
if (source != WiimoteSource::OpenXR)
{
auto* warning_layout = new QHBoxLayout();
auto* warning_label =
new QLabel(tr("The controls under Accelerometer and Gyroscope are designed to "
"interface directly with motion sensor hardware. They are not intended for "
"mapping traditional buttons, triggers or axes. You might need to configure "
"alternate input sources before using these controls."));
warning_label->setWordWrap(true);
auto* warning_input_sources_button = new QPushButton(tr("Alternate Input Sources"));
warning_layout->addWidget(
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxWarning, warning_label), 1);
warning_layout->addWidget(warning_input_sources_button);
connect(warning_input_sources_button, &QPushButton::clicked, this, [this] {
ControllerInterfaceWindow window{this};
window.exec();
});
m_main_layout->addLayout(warning_layout);
}
m_main_layout->addLayout(groups_layout);
setLayout(m_main_layout);
@@ -13,12 +13,16 @@
#include "DiscIO/Volume.h"
#include "DolphinQt/Config/ARCodeWidget.h"
#include "DolphinQt/Config/ElementsGroupOverrideWidget.h"
#include "DolphinQt/Config/FilesystemWidget.h"
#include "DolphinQt/Config/GameConfigWidget.h"
#include "DolphinQt/Config/GeckoCodeWidget.h"
#include "DolphinQt/Config/GraphicsModListWidget.h"
#include "DolphinQt/Config/VRConfigWidget.h"
#include "DolphinQt/Config/InfoWidget.h"
#include "DolphinQt/Config/PatchesWidget.h"
#include "DolphinQt/Config/HideObjectWidget.h"
#include "DolphinQt/Config/ShaderOverrideWidget.h"
#include "DolphinQt/Config/VerifyWidget.h"
#include "DolphinQt/QtUtils/WrapInScrollArea.h"
@@ -38,6 +42,7 @@ PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& ga
new GeckoCodeWidget(game.GetGameID(), game.GetGameTDBID(), game.GetRevision());
auto* const patches = new PatchesWidget(game);
auto* const game_config = new GameConfigWidget(game);
auto* const vr_config = new VRConfigWidget(game.GetGameID());
auto* const graphics_mod_list = new GraphicsModListWidget(game);
connect(gecko, &GeckoCodeWidget::OpenGeneralSettings, this,
@@ -59,11 +64,21 @@ PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& ga
// Note: Intentional selective use of AddWrappedPane for a sensible dialog "minimumSize".
AddWrappedPane(info, tr("Info"));
AddPane(game_config, tr("Game Config"));
AddPane(vr_config, tr("VR Config"));
AddPane(patches, tr("Patches"));
AddPane(ar, tr("AR Codes"));
AddPane(gecko, tr("Gecko Codes"));
AddWrappedPane(graphics_mod_list, tr("Graphics Mods"));
auto* const shader_overrides = new ShaderOverrideWidget(game.GetGameID());
AddPane(shader_overrides, tr("Shader Overrides"));
auto* const element_group_overrides = new ElementsGroupOverrideWidget(game.GetGameID());
AddPane(element_group_overrides, tr("Elements Group Overrides"));
auto* const hide_objects = new HideObjectWidget(game.GetGameID());
AddPane(hide_objects, tr("Hide Objects"));
if (game.GetPlatform() != DiscIO::Platform::ELFOrDOL)
{
std::shared_ptr<DiscIO::Volume> volume = DiscIO::CreateVolume(game.GetFilePath());
@@ -30,6 +30,7 @@
#include "DolphinQt/Settings/OnScreenDisplayPane.h"
#include "DolphinQt/Settings/PathPane.h"
#include "DolphinQt/Settings/TriforcePane.h"
#include "DolphinQt/Settings/VRPane.h"
#include "DolphinQt/Settings/WiiPane.h"
StackedSettingsWindow::StackedSettingsWindow(QWidget* parent) : QDialog{parent}
@@ -193,6 +194,7 @@ SettingsWindow::SettingsWindow(MainWindow* parent) : StackedSettingsWindow{paren
// If you change the order, don't forget to update the SettingsWindowPaneIndex enum.
AddWrappedPane(new GeneralPane, tr("General"));
AddPane(new GraphicsPane{parent, nullptr}, tr("Graphics"));
AddWrappedPane(new VRPane, tr("VR"));
AddWrappedPane(new ControllersPane, tr("Controllers"));
AddWrappedPane(new InterfacePane, tr("Interface"));
AddWrappedPane(new OnScreenDisplayPane, tr("On-Screen Display"));
@@ -42,6 +42,7 @@ enum class SettingsWindowPaneIndex : int
{
General = 0,
Graphics,
VR,
Controllers,
Interface,
OnScreenDisplay,
@@ -0,0 +1,853 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ShaderOverrideAddEditDialog.h"
#include <algorithm>
#include <cctype>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <QCheckBox>
#include <QComboBox>
#include <QBrush>
#include <QColor>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollArea>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QWidget>
#include <QVBoxLayout>
#include <fmt/format.h>
#include "Common/FileUtil.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/ConfigManager.h"
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
namespace
{
constexpr double UPM_OVERRIDE_MIN = 0.01;
constexpr double UPM_OVERRIDE_MAX = 1000.0;
constexpr double UPM_OVERRIDE_STEP = 0.01;
constexpr int MAX_VISIBLE_TEXTURE_HASH_ROWS = 10;
constexpr int TEXTURE_HASH_ROW_HEIGHT = 28;
} // namespace
ShaderOverrideAddEditDialog::ShaderOverrideAddEditDialog(
QWidget* parent, const ShaderHunter::ShaderOverride* edit_override,
const std::vector<std::string>& available_flags)
: QDialog(parent)
{
setWindowTitle(edit_override ? tr("Edit Shader Override") : tr("Add Shader Override"));
setMinimumWidth(350);
m_name_edit = new QLineEdit;
m_name_edit->setPlaceholderText(tr("Override name..."));
m_comments_edit = new QPlainTextEdit;
m_comments_edit->setPlaceholderText(tr("Optional notes..."));
m_comments_edit->setTabChangesFocus(true);
m_comments_edit->setMinimumHeight(70);
m_credits_edit = new QLineEdit;
m_credits_edit->setPlaceholderText(tr("Optional author/credits..."));
m_hash_edit = new QLineEdit;
m_hash_edit->setPlaceholderText(tr("Hex hash (e.g. 0012abcd)"));
m_type_combo = new QComboBox;
m_type_combo->addItem(tr("Pixel Shader"), static_cast<int>(ShaderHunter::ShaderType::Pixel));
m_type_combo->addItem(tr("Vertex Shader"), static_cast<int>(ShaderHunter::ShaderType::Vertex));
m_type_combo->addItem(tr("Geometry Shader"),
static_cast<int>(ShaderHunter::ShaderType::Geometry));
m_match_mode_label = new QLabel(tr("Match Mode:"));
m_match_mode_combo = new QComboBox;
m_match_mode_combo->addItem(tr("Exact Hash"),
static_cast<int>(ShaderHunter::MatchMode::ExactHash));
m_match_mode_combo->addItem(tr("Shader Family"),
static_cast<int>(ShaderHunter::MatchMode::ShaderFamily));
m_match_mode_combo->setToolTip(
tr("Exact Hash: match only this exact shader hash.\n"
"Shader Family: use the relaxed shader family signature."));
m_handling_combo = new QComboBox;
m_handling_combo->addItem(tr("Skip"), static_cast<int>(ShaderHunter::HandlingType::Skip));
m_handling_combo->addItem(tr("Screen"), static_cast<int>(ShaderHunter::HandlingType::Screen));
m_handling_combo->addItem(tr("Fullscreen"),
static_cast<int>(ShaderHunter::HandlingType::Fullscreen));
m_handling_combo->addItem(tr("Head Locked"),
static_cast<int>(ShaderHunter::HandlingType::HeadLocked));
m_handling_combo->addItem(tr("Flag"), static_cast<int>(ShaderHunter::HandlingType::Flag));
m_handling_combo->addItem(tr("Units per Meter"),
static_cast<int>(ShaderHunter::HandlingType::UnitsPerMeter));
m_layer_label = new QLabel(tr("Layer:"));
m_layer_spin = new QSpinBox;
m_layer_spin->setRange(-1, 999);
m_layer_spin->setSpecialValueText(tr("Auto"));
m_layer_spin->setValue(-1);
m_layer_spin->setToolTip(tr("Manual depth layer for Screen/Head Locked handling.\n"
"-1 (Auto) = automatic per-draw counter.\n"
"Higher values are closer to camera."));
m_element_depth_label = new QLabel(tr("Element Depth:"));
m_element_depth_spin = new QDoubleSpinBox;
m_element_depth_spin->setRange(-1.0, 0.01);
m_element_depth_spin->setDecimals(4);
m_element_depth_spin->setSingleStep(0.0001);
m_element_depth_spin->setSpecialValueText(tr("Global"));
m_element_depth_spin->setValue(-1.0);
m_element_depth_spin->setToolTip(tr("Within-element depth range for this shader.\n"
"-1 (Global) = use the global Element Depth setting.\n"
"Higher values fix Z-fighting inside the element."));
m_units_per_meter_label = new QLabel(tr("Units per Meter:"));
m_units_per_meter_spin = new QDoubleSpinBox;
m_units_per_meter_spin->setRange(UPM_OVERRIDE_MIN, UPM_OVERRIDE_MAX);
m_units_per_meter_spin->setDecimals(2);
m_units_per_meter_spin->setSingleStep(UPM_OVERRIDE_STEP);
m_units_per_meter_spin->setValue(1.0);
m_units_per_meter_spin->setToolTip(tr("Temporary per-shader scale override for VR.\n"
"Higher values make this shader appear larger."));
m_flag_label = new QLabel(tr("Flag Group:"));
m_flag_edit = new QLineEdit;
m_flag_edit->setPlaceholderText(tr("e.g. gameplay"));
m_flag_edit->setToolTip(tr("When this shader is drawn, it sets this named flag.\n"
"Other overrides can be conditional on this flag.\n"
"Optional for non-Flag handling (shader acts as both override and flag)."));
m_condition_label = new QLabel(tr("Condition:"));
m_condition_combo = new QComboBox;
m_condition_combo->addItem(tr("(None)"), QString());
for (const auto& flag : available_flags)
m_condition_combo->addItem(QString::fromStdString(flag), QString::fromStdString(flag));
m_condition_combo->setToolTip(
tr("Select a flag condition for this override.\n"
"Use Condition Mode to choose active or inactive behavior.\n"
"Flags are detected from the current and previous frame."));
m_condition_mode_label = new QLabel(tr("Condition Mode:"));
m_condition_mode_combo = new QComboBox;
m_condition_mode_combo->addItem(tr("Activate"), false);
m_condition_mode_combo->addItem(tr("Deactivate"), true);
m_condition_mode_combo->setToolTip(
tr("Activate: apply when the selected flag is active.\n"
"Deactivate: apply when the selected flag is NOT active."));
m_clear_efb_check = new QCheckBox(tr("Clear EFB Copy"));
m_clear_efb_check->setToolTip(
tr("When this shader is drawn, the next EFB copy will be cleared to\n"
"transparent instead of copying the framebuffer content.\n"
"Use this to remove post-processing effects (bloom, blur) in VR.\n"
"Can be combined with any handling type (Skip, Screen, etc.)."));
m_clear_efb_min_label = new QLabel(tr("EFB Min Width:"));
m_clear_efb_min_spin = new QSpinBox;
m_clear_efb_min_spin->setRange(0, 640);
m_clear_efb_min_spin->setSingleStep(10);
m_clear_efb_min_spin->setSpecialValueText(tr("Any"));
m_clear_efb_min_spin->setValue(0);
m_clear_efb_min_spin->setToolTip(tr("Only clear EFB copies whose native width >= this value.\n"
"0 (Any) = no lower bound.\n"
"Full EFB = 640 wide; set ~400 to only clear full-screen effects."));
m_clear_efb_max_label = new QLabel(tr("EFB Max Width:"));
m_clear_efb_max_spin = new QSpinBox;
m_clear_efb_max_spin->setRange(0, 640);
m_clear_efb_max_spin->setSingleStep(10);
m_clear_efb_max_spin->setSpecialValueText(tr("Any"));
m_clear_efb_max_spin->setValue(0);
m_clear_efb_max_spin->setToolTip(tr("Only clear EFB copies whose native width <= this value.\n"
"0 (Any) = no upper bound.\n"
"Use to restrict clearing to small or partial EFB copies."));
m_element_start_label = new QLabel(tr("Draw Call Start:"));
m_element_start_spin = new QSpinBox;
m_element_start_spin->setRange(0, 9999);
m_element_start_spin->setValue(0);
m_element_start_spin->setToolTip(
tr("First draw call index to apply this override.\n"
"Only used when Draw Call Filter is enabled.\n"
"Use Draw Call mode in Shader Hunter to identify draw call indices."));
m_element_end_label = new QLabel(tr("Draw Call End:"));
m_element_end_spin = new QSpinBox;
m_element_end_spin->setRange(0, 9999);
m_element_end_spin->setValue(0);
m_element_end_spin->setToolTip(
tr("Last draw call index to apply this override.\n"
"Only used when Draw Call Filter is enabled.\n"
"Range is inclusive: start=2, end=5 skips draws 2,3,4,5."));
m_element_filter_check = new QCheckBox(tr("Draw Call Filter"));
m_element_filter_check->setToolTip(
tr("Enable to apply this override only to a specific draw-call range.\n"
"Ranges adapt automatically when draw-call counts change between frames."));
m_hash_family_check = new QCheckBox(tr("Match Shader Family"));
m_hash_family_check->setToolTip(
tr("Match this override against a relaxed shader family signature instead of the exact hash.\n"
"Useful when the same effect changes hash across scenes or game revisions."));
m_hash_family_check->hide();
m_view_textures_button = new QPushButton(tr("View Textures"));
m_view_textures_button->setToolTip(
tr("Show captured textures for this shader hash and type.\n"
"Check textures in the popup and apply them to Texture Filters."));
m_texture_mode_label = new QLabel(tr("Texture Filter Mode:"));
m_texture_mode_combo = new QComboBox;
m_texture_mode_combo->addItem(tr("Include"), false);
m_texture_mode_combo->addItem(tr("Exclude"), true);
m_texture_mode_combo->setToolTip(
tr("Include: apply override only when any listed texture hash is bound.\n"
"Exclude: apply override only when none of the listed texture hashes are bound."));
m_texture_hash_container = new QWidget;
m_texture_hash_layout = new QVBoxLayout(m_texture_hash_container);
m_texture_hash_layout->setContentsMargins(0, 0, 0, 0);
m_texture_hash_layout->setSpacing(4);
m_texture_hash_scroll = new QScrollArea;
m_texture_hash_scroll->setWidgetResizable(true);
m_texture_hash_scroll->setFrameShape(QFrame::NoFrame);
m_texture_hash_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_texture_hash_scroll->setWidget(m_texture_hash_container);
AddTextureHashField(QString());
// Pre-fill fields in edit mode
if (edit_override)
{
m_edit_element_reference_total = edit_override->element_reference_total;
m_edit_family_signature = edit_override->family_signature;
m_name_edit->setText(QString::fromStdString(edit_override->name));
m_comments_edit->setPlainText(QString::fromStdString(edit_override->comments));
m_credits_edit->setText(QString::fromStdString(edit_override->credits));
m_hash_edit->setText(
QString::fromStdString(fmt::format("{:016x}", edit_override->hash)));
const int type_idx = m_type_combo->findData(static_cast<int>(edit_override->type));
if (type_idx >= 0)
m_type_combo->setCurrentIndex(type_idx);
const ShaderHunter::MatchMode initial_match_mode =
edit_override->match_mode == ShaderHunter::MatchMode::RuntimeElement ?
ShaderHunter::MatchMode::ExactHash :
edit_override->match_mode;
const int match_mode_idx =
m_match_mode_combo->findData(static_cast<int>(initial_match_mode));
if (match_mode_idx >= 0)
m_match_mode_combo->setCurrentIndex(match_mode_idx);
const int handling_idx = m_handling_combo->findData(static_cast<int>(edit_override->handling));
if (handling_idx >= 0)
m_handling_combo->setCurrentIndex(handling_idx);
m_layer_spin->setValue(edit_override->layer);
m_element_depth_spin->setValue(edit_override->element_depth);
if (edit_override->units_per_meter > 0.0f)
m_units_per_meter_spin->setValue(edit_override->units_per_meter);
m_flag_edit->setText(QString::fromStdString(edit_override->flag_group));
m_clear_efb_check->setChecked(edit_override->clear_efb);
m_clear_efb_min_spin->setValue(edit_override->clear_efb_min_width);
m_clear_efb_max_spin->setValue(edit_override->clear_efb_max_width);
const bool has_element_filter =
edit_override->element_start >= 0 || edit_override->element_end >= 0;
m_element_filter_check->setChecked(has_element_filter);
m_hash_family_check->setChecked(edit_override->hash_family_match);
m_element_start_spin->setValue(std::max(0, edit_override->element_start));
m_element_end_spin->setValue(std::max(0, edit_override->element_end));
m_updating_texture_hash_fields = true;
while (m_texture_hash_edits.size() < edit_override->texture_hashes.size())
AddTextureHashField(QString());
for (size_t i = 0; i < edit_override->texture_hashes.size(); i++)
{
m_texture_hash_edits[i]->setText(
QString::fromStdString(fmt::format("{:016x}", edit_override->texture_hashes[i])));
}
for (size_t i = edit_override->texture_hashes.size(); i < m_texture_hash_edits.size(); i++)
m_texture_hash_edits[i]->clear();
m_updating_texture_hash_fields = false;
EnsureTextureHashFieldRows();
const int texture_mode_idx =
m_texture_mode_combo->findData(edit_override->texture_hashes_excluded);
if (texture_mode_idx >= 0)
m_texture_mode_combo->setCurrentIndex(texture_mode_idx);
if (!edit_override->condition_flag.empty())
{
const int cond_idx =
m_condition_combo->findData(QString::fromStdString(edit_override->condition_flag));
if (cond_idx >= 0)
m_condition_combo->setCurrentIndex(cond_idx);
else
{
// Flag not in available list — add it dynamically
m_condition_combo->addItem(QString::fromStdString(edit_override->condition_flag),
QString::fromStdString(edit_override->condition_flag));
m_condition_combo->setCurrentIndex(m_condition_combo->count() - 1);
}
}
const int mode_idx = m_condition_mode_combo->findData(edit_override->condition_inverted);
if (mode_idx >= 0)
m_condition_mode_combo->setCurrentIndex(mode_idx);
}
auto* form = new QFormLayout;
form->addRow(tr("Name:"), m_name_edit);
form->addRow(tr("Hash:"), m_hash_edit);
form->addRow(tr("Shader Type:"), m_type_combo);
form->addRow(m_match_mode_label, m_match_mode_combo);
form->addRow(tr("Handling:"), m_handling_combo);
form->addRow(m_layer_label, m_layer_spin);
form->addRow(m_element_depth_label, m_element_depth_spin);
form->addRow(m_units_per_meter_label, m_units_per_meter_spin);
form->addRow(m_flag_label, m_flag_edit);
form->addRow(m_condition_label, m_condition_combo);
form->addRow(m_condition_mode_label, m_condition_mode_combo);
form->addRow(QString(), m_clear_efb_check);
form->addRow(m_clear_efb_min_label, m_clear_efb_min_spin);
form->addRow(m_clear_efb_max_label, m_clear_efb_max_spin);
form->addRow(QString(), m_element_filter_check);
form->addRow(m_element_start_label, m_element_start_spin);
form->addRow(m_element_end_label, m_element_end_spin);
form->addRow(QString(), m_view_textures_button);
form->addRow(m_texture_mode_label, m_texture_mode_combo);
form->addRow(tr("Texture Filters:"), m_texture_hash_scroll);
form->addRow(tr("Comments:"), m_comments_edit);
form->addRow(tr("Credits:"), m_credits_edit);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &ShaderOverrideAddEditDialog::OnAccept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_handling_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ShaderOverrideAddEditDialog::OnHandlingChanged);
connect(m_clear_efb_check, &QCheckBox::toggled, this,
&ShaderOverrideAddEditDialog::OnClearEFBChanged);
connect(m_element_filter_check, &QCheckBox::toggled, this,
&ShaderOverrideAddEditDialog::OnElementFilterChanged);
connect(m_condition_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int) {
const bool has_condition = !m_condition_combo->currentData().toString().isEmpty();
m_condition_mode_label->setEnabled(has_condition);
m_condition_mode_combo->setEnabled(has_condition);
});
connect(m_view_textures_button, &QPushButton::clicked, this,
&ShaderOverrideAddEditDialog::ShowTextureBrowser);
auto* layout = new QVBoxLayout;
layout->addLayout(form);
layout->addWidget(buttons);
setLayout(layout);
// Show/hide fields based on initial handling selection
OnHandlingChanged();
const bool has_condition = !m_condition_combo->currentData().toString().isEmpty();
m_condition_mode_label->setEnabled(has_condition);
m_condition_mode_combo->setEnabled(has_condition);
OnClearEFBChanged();
OnElementFilterChanged();
}
ShaderHunter::ShaderOverride ShaderOverrideAddEditDialog::GetResult() const
{
ShaderHunter::ShaderOverride result;
result.name = m_name_edit->text().toStdString();
result.comments = m_comments_edit->toPlainText().trimmed().toStdString();
result.credits = m_credits_edit->text().trimmed().toStdString();
result.hash = std::strtoull(m_hash_edit->text().toStdString().c_str(), nullptr, 16);
result.type =
static_cast<ShaderHunter::ShaderType>(m_type_combo->currentData().toInt());
result.match_mode =
static_cast<ShaderHunter::MatchMode>(m_match_mode_combo->currentData().toInt());
result.hash_family_match = false;
result.family_signature = 0;
if (result.match_mode == ShaderHunter::MatchMode::ShaderFamily)
{
result.hash_family_match = true;
result.family_signature = m_edit_family_signature;
if (const auto signature =
ShaderHunter::GetInstance().GetShaderFamilySignature(result.type, result.hash);
signature.has_value())
{
result.family_signature = *signature;
}
}
result.handling =
static_cast<ShaderHunter::HandlingType>(m_handling_combo->currentData().toInt());
result.layer = (result.handling == ShaderHunter::HandlingType::Screen ||
result.handling == ShaderHunter::HandlingType::HeadLocked) ?
m_layer_spin->value() : -1;
result.element_depth = (result.handling == ShaderHunter::HandlingType::Screen ||
result.handling == ShaderHunter::HandlingType::HeadLocked) ?
static_cast<float>(m_element_depth_spin->value()) : -1.0f;
result.units_per_meter = result.handling == ShaderHunter::HandlingType::UnitsPerMeter ?
static_cast<float>(m_units_per_meter_spin->value()) :
-1.0f;
result.clear_efb = m_clear_efb_check->isChecked();
result.clear_efb_min_width = result.clear_efb ? m_clear_efb_min_spin->value() : 0;
result.clear_efb_max_width = result.clear_efb ? m_clear_efb_max_spin->value() : 0;
if (m_element_filter_check->isChecked())
{
result.element_start = m_element_start_spin->value();
result.element_end = m_element_end_spin->value();
result.element_reference_total = m_edit_element_reference_total;
}
else
{
result.element_start = -1;
result.element_end = -1;
result.element_reference_total = 0;
}
const auto texture_tokens = CollectTextureHashTokens();
for (const std::string& token : texture_tokens)
{
const u64 parsed = std::strtoull(token.c_str(), nullptr, 16);
if (parsed != 0)
result.texture_hashes.push_back(parsed);
}
std::sort(result.texture_hashes.begin(), result.texture_hashes.end());
result.texture_hashes.erase(
std::unique(result.texture_hashes.begin(), result.texture_hashes.end()),
result.texture_hashes.end());
result.texture_hashes_excluded =
!result.texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
result.enabled = true;
result.user_defined = true;
// flag_group is available on all handling types (optional except for Flag)
result.flag_group = m_flag_edit->text().trimmed().toStdString();
if (result.handling != ShaderHunter::HandlingType::Flag)
{
result.condition_flag = m_condition_combo->currentData().toString().toStdString();
result.condition_inverted =
!result.condition_flag.empty() && m_condition_mode_combo->currentData().toBool();
}
return result;
}
void ShaderOverrideAddEditDialog::OnAccept()
{
const std::string name = m_name_edit->text().toStdString();
if (name.empty())
{
QMessageBox::warning(this, tr("Validation Error"), tr("Name cannot be empty."));
return;
}
const std::string hash_str = m_hash_edit->text().toStdString();
if (hash_str.empty())
{
QMessageBox::warning(this, tr("Validation Error"), tr("Hash cannot be empty."));
return;
}
// Validate hex string
for (char c : hash_str)
{
if (!std::isxdigit(static_cast<unsigned char>(c)))
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Hash must contain only hexadecimal characters (0-9, a-f)."));
return;
}
}
if (hash_str.size() > 16)
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Hash must be at most 16 hex digits."));
return;
}
// Validate texture hash hex string (if provided)
const auto texture_tokens = CollectTextureHashTokens();
for (const std::string& token : texture_tokens)
{
for (char c : token)
{
if (!std::isxdigit(static_cast<unsigned char>(c)))
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Texture filter values must contain only hexadecimal characters "
"(0-9, a-f)."));
return;
}
}
if (token.size() > 16)
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Each texture filter value must be at most 16 hex digits."));
return;
}
}
// Validate flag group name for Flag handling
if (m_element_filter_check->isChecked() &&
m_element_start_spin->value() > m_element_end_spin->value())
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Draw Call Start must be less than or equal to Draw Call End."));
return;
}
// Validate flag group name for Flag handling
const auto handling = static_cast<ShaderHunter::HandlingType>(
m_handling_combo->currentData().toInt());
if (handling == ShaderHunter::HandlingType::Flag && m_flag_edit->text().trimmed().isEmpty())
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Flag Group name cannot be empty for Flag handling."));
return;
}
if (handling == ShaderHunter::HandlingType::UnitsPerMeter &&
m_units_per_meter_spin->value() <= 0.0)
{
QMessageBox::warning(this, tr("Validation Error"),
tr("Units per Meter must be greater than 0."));
return;
}
accept();
}
void ShaderOverrideAddEditDialog::OnHandlingChanged()
{
const auto handling = static_cast<ShaderHunter::HandlingType>(
m_handling_combo->currentData().toInt());
const bool show_layer = (handling == ShaderHunter::HandlingType::Screen ||
handling == ShaderHunter::HandlingType::HeadLocked);
const bool show_units_per_meter = (handling == ShaderHunter::HandlingType::UnitsPerMeter);
const bool is_flag = (handling == ShaderHunter::HandlingType::Flag);
m_layer_label->setVisible(show_layer);
m_layer_spin->setVisible(show_layer);
m_element_depth_label->setVisible(show_layer);
m_element_depth_spin->setVisible(show_layer);
m_units_per_meter_label->setVisible(show_units_per_meter);
m_units_per_meter_spin->setVisible(show_units_per_meter);
// Flag group is always visible (optional for non-Flag handling, required for Flag)
m_flag_label->setVisible(true);
m_flag_edit->setVisible(true);
m_condition_label->setVisible(!is_flag);
m_condition_combo->setVisible(!is_flag);
m_condition_mode_label->setVisible(!is_flag);
m_condition_mode_combo->setVisible(!is_flag);
}
void ShaderOverrideAddEditDialog::OnClearEFBChanged()
{
const bool show = m_clear_efb_check->isChecked();
m_clear_efb_min_label->setVisible(show);
m_clear_efb_min_spin->setVisible(show);
m_clear_efb_max_label->setVisible(show);
m_clear_efb_max_spin->setVisible(show);
}
void ShaderOverrideAddEditDialog::OnElementFilterChanged()
{
const bool show = m_element_filter_check->isChecked();
m_element_start_label->setVisible(show);
m_element_start_spin->setVisible(show);
m_element_end_label->setVisible(show);
m_element_end_spin->setVisible(show);
}
void ShaderOverrideAddEditDialog::ShowTextureBrowser()
{
const std::string hash_str = m_hash_edit->text().trimmed().toStdString();
if (hash_str.empty())
{
QMessageBox::warning(this, tr("View Textures"),
tr("Hash cannot be empty. Enter the shader hash first."));
return;
}
if (hash_str.size() > 16)
{
QMessageBox::warning(this, tr("View Textures"),
tr("Hash must be at most 16 hex digits."));
return;
}
for (char c : hash_str)
{
if (!std::isxdigit(static_cast<unsigned char>(c)))
{
QMessageBox::warning(this, tr("View Textures"),
tr("Hash must contain only hexadecimal characters (0-9, a-f)."));
return;
}
}
const u64 shader_hash = std::strtoull(hash_str.c_str(), nullptr, 16);
const auto shader_type =
static_cast<ShaderHunter::ShaderType>(m_type_combo->currentData().toInt());
auto& hunter = ShaderHunter::GetInstance();
const bool was_hunting_enabled = hunter.IsEnabled();
const auto previous_active_type = hunter.GetActiveType();
const int previous_selected_pos = hunter.GetSelectedPosition();
const u64 previous_selected_hash = hunter.GetSelectedHash();
const char* type_str = shader_type == ShaderHunter::ShaderType::Pixel ? "PS" :
shader_type == ShaderHunter::ShaderType::Vertex ? "VS" :
"GS";
if (!was_hunting_enabled)
hunter.SetEnabled(true);
hunter.SetTextureToolActive(true);
const auto initial_hashes = CollectTextureHashValues();
const auto sync_selected_hashes = [shader_hash, shader_type](const std::vector<u64>& hashes) {
auto& sync_hunter = ShaderHunter::GetInstance();
if (!sync_hunter.SelectShader(shader_type, shader_hash))
return;
sync_hunter.ClearTextureSkipFilters();
for (u64 selected_hash : hashes)
sync_hunter.SetTextureSkipEnabled(selected_hash, true);
};
TextureHashBrowserConfig browser_config;
browser_config.title = tr("Textures for %1 %2")
.arg(QString::fromLatin1(type_str))
.arg(static_cast<qulonglong>(shader_hash), 16, 16,
QLatin1Char('0'));
browser_config.empty_info_text =
tr("No textures captured yet for this shader.\n"
"Shader Hunter was enabled temporarily for this window.");
browser_config.current_label = tr("%1 %2")
.arg(QString::fromLatin1(type_str))
.arg(static_cast<qulonglong>(shader_hash), 16, 16,
QLatin1Char('0'));
browser_config.initial_selected_hashes = initial_hashes;
browser_config.fetch_current_entries = [shader_hash, shader_type]() {
auto& populate_hunter = ShaderHunter::GetInstance();
if (!populate_hunter.SelectShader(shader_type, shader_hash))
return std::vector<TextureHashBrowserEntry>{};
std::vector<TextureHashBrowserEntry> entries;
for (const auto& tex : populate_hunter.GetTexturesForHash(shader_type, shader_hash))
entries.push_back(TextureHashBrowserEntry{.hash = tex.hash, .name = tex.name});
return entries;
};
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
SetTextureHashFields(hashes);
};
browser_config.live_selection_changed = sync_selected_hashes;
auto* dlg = ShowTextureHashBrowserDialog(this, browser_config);
connect(dlg, &QObject::destroyed, this,
[was_hunting_enabled, previous_active_type, previous_selected_pos,
previous_selected_hash]() {
auto& restore_hunter = ShaderHunter::GetInstance();
restore_hunter.SetTextureToolActive(false);
restore_hunter.ClearTextureSkipFilters();
if (previous_selected_pos >= 0)
{
if (!restore_hunter.SelectShader(previous_active_type, previous_selected_hash))
restore_hunter.SetActiveType(previous_active_type);
}
else
{
restore_hunter.SetActiveType(previous_active_type);
}
if (!was_hunting_enabled)
restore_hunter.SetEnabled(false);
});
}
std::vector<std::string> ShaderOverrideAddEditDialog::CollectTextureHashTokens() const
{
std::vector<std::string> tokens;
const QRegularExpression separators(QStringLiteral("[,;\\s]+"));
const QRegularExpression hex_exact(QStringLiteral("^[0-9A-Fa-f]{1,16}$"));
const QRegularExpression hex_16_anywhere(QStringLiteral("([0-9A-Fa-f]{16})"));
for (QLineEdit* edit : m_texture_hash_edits)
{
if (edit == nullptr)
continue;
const auto parts = edit->text().trimmed().split(separators, Qt::SkipEmptyParts);
for (const QString& part : parts)
{
if (hex_exact.match(part).hasMatch())
{
tokens.push_back(part.toLower().toStdString());
continue;
}
const auto match = hex_16_anywhere.match(part);
if (match.hasMatch())
{
tokens.push_back(match.captured(1).toLower().toStdString());
continue;
}
tokens.push_back(part.toStdString());
}
}
return tokens;
}
std::vector<u64> ShaderOverrideAddEditDialog::CollectTextureHashValues() const
{
std::vector<u64> hashes;
for (const std::string& token : CollectTextureHashTokens())
{
if (token.empty() || token.size() > 16)
continue;
bool valid = true;
for (char c : token)
{
if (!std::isxdigit(static_cast<unsigned char>(c)))
{
valid = false;
break;
}
}
if (!valid)
continue;
const u64 parsed = std::strtoull(token.c_str(), nullptr, 16);
if (parsed != 0)
hashes.push_back(parsed);
}
std::sort(hashes.begin(), hashes.end());
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
return hashes;
}
void ShaderOverrideAddEditDialog::SetTextureHashFields(const std::vector<u64>& hashes)
{
m_updating_texture_hash_fields = true;
while (m_texture_hash_edits.size() < hashes.size())
AddTextureHashField(QString());
for (size_t i = 0; i < hashes.size(); i++)
{
m_texture_hash_edits[i]->setText(
QString::fromStdString(fmt::format("{:016x}", hashes[i])));
}
for (size_t i = hashes.size(); i < m_texture_hash_edits.size(); i++)
m_texture_hash_edits[i]->clear();
m_updating_texture_hash_fields = false;
EnsureTextureHashFieldRows();
}
void ShaderOverrideAddEditDialog::AddTextureHashField(const QString& text)
{
auto* edit = new QLineEdit;
edit->setPlaceholderText(tr("Hex hash"));
edit->setToolTip(
tr("Optional texture filter hash.\n"
"Enter one hash per line; a new line is added automatically.\n"
"You can also paste multiple hashes separated by comma, semicolon, or spaces."));
edit->setText(text);
m_texture_hash_layout->addWidget(edit);
m_texture_hash_edits.push_back(edit);
connect(edit, &QLineEdit::textChanged, this, [this, edit](const QString&) {
SanitizeTextureHashField(edit);
EnsureTextureHashFieldRows();
});
}
void ShaderOverrideAddEditDialog::EnsureTextureHashFieldRows()
{
if (m_updating_texture_hash_fields)
return;
m_updating_texture_hash_fields = true;
int last_non_empty = -1;
for (int i = 0; i < static_cast<int>(m_texture_hash_edits.size()); i++)
{
if (!m_texture_hash_edits[i]->text().trimmed().isEmpty())
last_non_empty = i;
}
const int desired_rows = std::max(1, last_non_empty + 2);
while (static_cast<int>(m_texture_hash_edits.size()) < desired_rows)
AddTextureHashField(QString());
while (static_cast<int>(m_texture_hash_edits.size()) > desired_rows)
{
QLineEdit* edit = m_texture_hash_edits.back();
m_texture_hash_edits.pop_back();
m_texture_hash_layout->removeWidget(edit);
delete edit;
}
const int visible_rows = std::min(desired_rows, MAX_VISIBLE_TEXTURE_HASH_ROWS);
const int viewport_height = visible_rows * TEXTURE_HASH_ROW_HEIGHT;
m_texture_hash_scroll->setMinimumHeight(viewport_height);
m_texture_hash_scroll->setMaximumHeight(viewport_height);
m_updating_texture_hash_fields = false;
}
void ShaderOverrideAddEditDialog::SanitizeTextureHashField(QLineEdit* edit)
{
if (edit == nullptr || m_updating_texture_hash_fields)
return;
const QRegularExpression separators(QStringLiteral("[,;\\s]+"));
const QRegularExpression hex_exact(QStringLiteral("^[0-9A-Fa-f]{1,16}$"));
const QRegularExpression hex_16_anywhere(QStringLiteral("([0-9A-Fa-f]{16})"));
const auto parts = edit->text().trimmed().split(separators, Qt::SkipEmptyParts);
if (parts.isEmpty())
return;
QStringList sanitized_parts;
bool changed = false;
for (const QString& part : parts)
{
if (hex_exact.match(part).hasMatch())
{
const QString lowered = part.toLower();
sanitized_parts.push_back(lowered);
if (lowered != part)
changed = true;
continue;
}
const auto match = hex_16_anywhere.match(part);
if (match.hasMatch())
{
sanitized_parts.push_back(match.captured(1).toLower());
changed = true;
continue;
}
sanitized_parts.push_back(part);
}
if (!changed)
return;
const QString new_text = sanitized_parts.join(QStringLiteral(" "));
if (new_text == edit->text())
return;
const QSignalBlocker blocker(edit);
edit->setText(new_text);
}
@@ -0,0 +1,89 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include "VideoCommon/ShaderHunter.h"
class QCheckBox;
class QComboBox;
class QDoubleSpinBox;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QScrollArea;
class QString;
class QSpinBox;
class QVBoxLayout;
class QWidget;
class ShaderOverrideAddEditDialog : public QDialog
{
Q_OBJECT
public:
// edit_override != nullptr → edit mode (pre-fills fields); nullptr → add mode.
explicit ShaderOverrideAddEditDialog(
QWidget* parent, const ShaderHunter::ShaderOverride* edit_override = nullptr,
const std::vector<std::string>& available_flags = {});
ShaderHunter::ShaderOverride GetResult() const;
private:
std::vector<std::string> CollectTextureHashTokens() const;
std::vector<u64> CollectTextureHashValues() const;
void SetTextureHashFields(const std::vector<u64>& hashes);
void AddTextureHashField(const QString& text);
void EnsureTextureHashFieldRows();
void SanitizeTextureHashField(QLineEdit* edit);
void ShowTextureBrowser();
void OnAccept();
void OnHandlingChanged();
void OnClearEFBChanged();
void OnElementFilterChanged();
QLineEdit* m_name_edit;
QPlainTextEdit* m_comments_edit;
QLineEdit* m_credits_edit;
QLineEdit* m_hash_edit;
QComboBox* m_type_combo;
QComboBox* m_match_mode_combo;
QLabel* m_match_mode_label;
QComboBox* m_handling_combo;
QSpinBox* m_layer_spin;
QLabel* m_layer_label;
QDoubleSpinBox* m_element_depth_spin;
QLabel* m_element_depth_label;
QDoubleSpinBox* m_units_per_meter_spin;
QLabel* m_units_per_meter_label;
QLineEdit* m_flag_edit;
QLabel* m_flag_label;
QComboBox* m_condition_combo;
QLabel* m_condition_label;
QComboBox* m_condition_mode_combo;
QLabel* m_condition_mode_label;
QCheckBox* m_clear_efb_check;
QSpinBox* m_clear_efb_min_spin;
QSpinBox* m_clear_efb_max_spin;
QLabel* m_clear_efb_min_label;
QLabel* m_clear_efb_max_label;
QSpinBox* m_element_start_spin;
QSpinBox* m_element_end_spin;
QLabel* m_element_start_label;
QLabel* m_element_end_label;
QCheckBox* m_element_filter_check;
QCheckBox* m_hash_family_check;
QPushButton* m_view_textures_button;
QComboBox* m_texture_mode_combo;
QLabel* m_texture_mode_label;
QWidget* m_texture_hash_container;
QScrollArea* m_texture_hash_scroll;
QVBoxLayout* m_texture_hash_layout;
std::vector<QLineEdit*> m_texture_hash_edits;
bool m_updating_texture_hash_fields = false;
int m_edit_element_reference_total = 0;
u64 m_edit_family_signature = 0;
};
@@ -0,0 +1,424 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/ShaderOverrideWidget.h"
#include <QHBoxLayout>
#include <QColor>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QStringList>
#include <QVBoxLayout>
#include <unordered_map>
#include "DolphinQt/Config/ShaderOverrideAddEditDialog.h"
#include "DolphinQt/Debugger/ShaderHunterWidget.h"
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
#include "DolphinQt/Settings.h"
#include "VideoCommon/ShaderHunter.h"
ShaderOverrideWidget::ShaderOverrideWidget(std::string game_id)
: m_game_id(std::move(game_id))
{
CreateWidgets();
ConnectWidgets();
LoadOverrides();
}
ShaderOverrideWidget::~ShaderOverrideWidget() = default;
void ShaderOverrideWidget::CreateWidgets()
{
m_code_list = new QListWidget;
auto* info_label = new QLabel(
tr("Shader overrides control how individual shaders are handled.\n"
"Use Add/Edit to create overrides manually, or add them from the Shader Hunter tool.\n"
"Handling: Skip = hide, Screen = world-fixed, Head Locked = follows head,\n"
"Fullscreen = no VR.\n"
"Units per Meter = temporary per-shader VR scale override.\n"
"Flag = detect shader presence to conditionally enable other overrides.\n"
"Clear EFB = clear the next EFB copy to transparent (removes post-processing effects)."));
info_label->setWordWrap(true);
m_code_add = new NonDefaultQPushButton(tr("&Add Override"));
m_code_edit = new NonDefaultQPushButton(tr("&Edit Override"));
m_code_edit->setEnabled(false);
m_code_remove = new NonDefaultQPushButton(tr("&Remove Override"));
m_code_remove->setEnabled(false);
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
m_code_reload = new NonDefaultQPushButton(tr("Reload &Shaders"));
m_shader_hunter = new NonDefaultQPushButton(tr("Shader Hunter"));
auto* button_layout = new QHBoxLayout;
button_layout->addWidget(m_shader_hunter);
button_layout->addWidget(m_code_add);
button_layout->addWidget(m_code_edit);
button_layout->addWidget(m_code_remove);
button_layout->addStretch();
button_layout->addWidget(m_code_refresh);
button_layout->addWidget(m_code_reload);
auto* layout = new QVBoxLayout{this};
layout->addWidget(info_label);
layout->addWidget(m_code_list);
layout->addLayout(button_layout);
}
void ShaderOverrideWidget::ConnectWidgets()
{
connect(m_code_list, &QListWidget::itemChanged, this, &ShaderOverrideWidget::OnItemChanged);
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
&ShaderOverrideWidget::OnSelectionChanged);
connect(m_code_add, &QPushButton::clicked, this, &ShaderOverrideWidget::OnAddClicked);
connect(m_code_edit, &QPushButton::clicked, this, &ShaderOverrideWidget::OnEditClicked);
connect(m_code_remove, &QPushButton::clicked, this, &ShaderOverrideWidget::OnRemoveClicked);
connect(m_code_refresh, &QPushButton::clicked, this, &ShaderOverrideWidget::OnRefreshClicked);
connect(m_code_reload, &QPushButton::clicked, this, &ShaderOverrideWidget::OnReloadClicked);
connect(&Settings::Instance(), &Settings::ShaderOverridesChanged, this,
[this] { LoadOverrides(); });
connect(m_shader_hunter, &QPushButton::clicked, this, [this] {
if (m_shader_hunter_widget)
{
m_shader_hunter_widget->show();
m_shader_hunter_widget->raise();
m_shader_hunter_widget->activateWindow();
return;
}
m_shader_hunter_widget = new ShaderHunterWidget(this);
m_shader_hunter_widget->setAttribute(Qt::WA_DeleteOnClose, true);
connect(m_shader_hunter_widget, &ShaderHunterWidget::OverridesChanged, this,
[this] { LoadOverrides(); });
m_shader_hunter_widget->show();
connect(m_shader_hunter_widget, &QObject::destroyed, this,
[this] { m_shader_hunter_widget = nullptr; });
});
}
void ShaderOverrideWidget::LoadOverrides()
{
m_overrides = ShaderHunter::LoadOverridesFromINI(m_game_id);
m_code_list->setEnabled(!m_game_id.empty());
m_code_remove->setEnabled(false);
m_code_edit->setEnabled(false);
UpdateList();
}
void ShaderOverrideWidget::SaveOverrides()
{
ShaderHunter::SaveOverridesToINI(m_game_id, m_overrides);
}
void ShaderOverrideWidget::UpdateList()
{
// Block signals to prevent itemChanged → SaveOverrides() during list construction.
const QSignalBlocker blocker(m_code_list);
m_code_list->clear();
struct HandlingHashKey
{
int handling = 0;
u64 hash = 0;
bool operator==(const HandlingHashKey& other) const
{
return handling == other.handling && hash == other.hash;
}
};
struct HandlingHashKeyHasher
{
size_t operator()(const HandlingHashKey& key) const noexcept
{
return std::hash<u64>{}(key.hash) ^ (std::hash<int>{}(key.handling) << 1);
}
};
std::unordered_map<u64, size_t> hash_counts;
std::unordered_map<HandlingHashKey, size_t, HandlingHashKeyHasher> handling_hash_counts;
hash_counts.reserve(m_overrides.size());
handling_hash_counts.reserve(m_overrides.size());
for (const auto& ovr : m_overrides)
{
++hash_counts[ovr.hash];
++handling_hash_counts[{static_cast<int>(ovr.handling), ovr.hash}];
}
// Build sorted display order: Flag overrides first, then their conditionals, then standalone.
std::vector<size_t> sorted_indices;
sorted_indices.reserve(m_overrides.size());
// 1. Overrides that set a flag (Flag-only or combined), then their conditionals
for (size_t i = 0; i < m_overrides.size(); i++)
{
if (!m_overrides[i].flag_group.empty())
{
sorted_indices.push_back(i);
// 2. Conditionals that depend on this flag
const auto& flag = m_overrides[i].flag_group;
for (size_t j = 0; j < m_overrides.size(); j++)
{
if (j != i && m_overrides[j].condition_flag == flag &&
std::find(sorted_indices.begin(), sorted_indices.end(), j) == sorted_indices.end())
sorted_indices.push_back(j);
}
}
}
// 3. Standalone overrides (no flag_group, no condition)
for (size_t i = 0; i < m_overrides.size(); i++)
{
if (m_overrides[i].flag_group.empty() && m_overrides[i].condition_flag.empty() &&
std::find(sorted_indices.begin(), sorted_indices.end(), i) == sorted_indices.end())
{
sorted_indices.push_back(i);
}
}
// 4. Any conditional overrides whose flag doesn't exist (orphaned)
for (size_t i = 0; i < m_overrides.size(); i++)
{
if (!m_overrides[i].condition_flag.empty() &&
std::find(sorted_indices.begin(), sorted_indices.end(), i) == sorted_indices.end())
{
sorted_indices.push_back(i);
}
}
for (size_t idx : sorted_indices)
{
const auto& ovr = m_overrides[idx];
const char* type_str = ovr.type == ShaderHunter::ShaderType::Vertex ? "VS" :
ovr.type == ShaderHunter::ShaderType::Geometry ? "GS" :
"PS";
const char* handling_str = ovr.handling == ShaderHunter::HandlingType::Screen ? "screen" :
ovr.handling == ShaderHunter::HandlingType::Fullscreen ||
ovr.handling == ShaderHunter::HandlingType::FullscreenMono ?
"fullscreen" :
ovr.handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" :
ovr.handling == ShaderHunter::HandlingType::Flag ? "flag" :
ovr.handling == ShaderHunter::HandlingType::UnitsPerMeter ?
"units_per_meter" :
"skip";
QString label;
if (ovr.handling == ShaderHunter::HandlingType::Flag)
{
label = QStringLiteral("%1 [%2 %3] (flag -> '%4')")
.arg(QString::fromStdString(ovr.name))
.arg(QString::fromLatin1(type_str))
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
.arg(QString::fromStdString(ovr.flag_group));
}
else if (!ovr.flag_group.empty())
{
// Non-Flag override that also sets a flag
label = QStringLiteral("%1 [%2 %3] (%4, flag -> '%5')")
.arg(QString::fromStdString(ovr.name))
.arg(QString::fromLatin1(type_str))
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
.arg(QString::fromLatin1(handling_str))
.arg(QString::fromStdString(ovr.flag_group));
}
else
{
label = QStringLiteral("%1 [%2 %3] (%4)")
.arg(QString::fromStdString(ovr.name))
.arg(QString::fromLatin1(type_str))
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
.arg(QString::fromLatin1(handling_str));
}
if (ovr.handling == ShaderHunter::HandlingType::Screen ||
ovr.handling == ShaderHunter::HandlingType::HeadLocked)
{
if (ovr.layer >= 0)
label += QStringLiteral(" L%1").arg(ovr.layer);
if (ovr.element_depth >= 0.0f)
label += QStringLiteral(" D%1").arg(ovr.element_depth, 0, 'f', 4);
}
else if (ovr.handling == ShaderHunter::HandlingType::UnitsPerMeter && ovr.units_per_meter > 0.0f)
{
label += QStringLiteral(" UPM%1").arg(ovr.units_per_meter, 0, 'f', 2);
}
if (ovr.hash_family_match)
label += QStringLiteral(" family");
if (ovr.clear_efb)
{
if (ovr.clear_efb_min_width > 0 || ovr.clear_efb_max_width > 0)
label += QStringLiteral(" +clearEFB(%1-%2)")
.arg(ovr.clear_efb_min_width)
.arg(ovr.clear_efb_max_width > 0 ? ovr.clear_efb_max_width : 640);
else
label += QStringLiteral(" +clearEFB");
}
if (ovr.element_start >= 0 && ovr.element_end >= 0)
{
label += QStringLiteral(" elem[%1-%2]").arg(ovr.element_start).arg(ovr.element_end);
if (ovr.element_reference_total > 0)
label += QStringLiteral("@%1").arg(ovr.element_reference_total);
}
if (!ovr.texture_hashes.empty())
{
QStringList texture_hashes;
for (u64 texture_hash : ovr.texture_hashes)
{
texture_hashes.append(
QString::number(texture_hash, 16).rightJustified(16, QLatin1Char('0')));
}
label += QStringLiteral(" tex(%1):%2")
.arg(ovr.texture_hashes_excluded ? QStringLiteral("exclude") :
QStringLiteral("include"))
.arg(texture_hashes.join(QStringLiteral(",")));
}
if (!ovr.condition_flag.empty())
{
const QString condition_text = ovr.condition_inverted ?
QStringLiteral("[when not '%1']").arg(
QString::fromStdString(ovr.condition_flag)) :
QStringLiteral("[when '%1']").arg(
QString::fromStdString(ovr.condition_flag));
label = QStringLiteral(" \u2514 ") + label + QStringLiteral(" ") + condition_text;
}
const bool has_duplicate_handling_hash =
handling_hash_counts[{static_cast<int>(ovr.handling), ovr.hash}] > 1;
const bool has_duplicate_hash = hash_counts[ovr.hash] > 1;
const bool has_hash_only_duplicate = has_duplicate_hash && !has_duplicate_handling_hash;
if (has_duplicate_handling_hash)
label += QStringLiteral(" [DUPLICATE HANDLING+HASH]");
else if (has_hash_only_duplicate)
label += QStringLiteral(" [DUPLICATE HASH]");
auto* item = new QListWidgetItem(label);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
item->setCheckState(ovr.enabled ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, static_cast<int>(idx));
if (has_duplicate_handling_hash)
{
item->setForeground(QColor(220, 85, 85));
item->setToolTip(
tr("Duplicate Handling+Hash: another override in this list has the same handling mode and hash."));
}
else if (has_hash_only_duplicate)
{
item->setForeground(QColor(214, 160, 0));
item->setToolTip(
tr("Duplicate Hash: another override in this list has the same hash but a different shader "
"type."));
}
m_code_list->addItem(item);
}
}
void ShaderOverrideWidget::OnItemChanged(QListWidgetItem* item)
{
const int idx = item->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
m_overrides[idx].enabled = (item->checkState() == Qt::Checked);
SaveOverrides();
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
}
void ShaderOverrideWidget::OnSelectionChanged()
{
const auto items = m_code_list->selectedItems();
const bool has_selection = !items.empty();
m_code_remove->setEnabled(has_selection);
m_code_edit->setEnabled(has_selection);
}
std::vector<std::string> ShaderOverrideWidget::CollectAvailableFlags() const
{
std::vector<std::string> flags;
for (const auto& ovr : m_overrides)
{
if (!ovr.flag_group.empty())
{
if (std::find(flags.begin(), flags.end(), ovr.flag_group) == flags.end())
flags.push_back(ovr.flag_group);
}
}
return flags;
}
void ShaderOverrideWidget::OnAddClicked()
{
ShaderOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags());
if (dialog.exec() != QDialog::Accepted)
return;
m_overrides.push_back(dialog.GetResult());
SaveOverrides();
UpdateList();
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
}
void ShaderOverrideWidget::OnEditClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int idx = items[0]->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
ShaderOverrideAddEditDialog dialog(this, &m_overrides[idx], CollectAvailableFlags());
if (dialog.exec() != QDialog::Accepted)
return;
auto result = dialog.GetResult();
result.enabled = m_overrides[idx].enabled; // Preserve enabled state
m_overrides[idx] = result;
SaveOverrides();
UpdateList();
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
}
void ShaderOverrideWidget::OnRemoveClicked()
{
const auto items = m_code_list->selectedItems();
if (items.empty())
return;
const int idx = items[0]->data(Qt::UserRole).toInt();
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
return;
m_overrides.erase(m_overrides.begin() + idx);
SaveOverrides();
UpdateList();
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
m_code_remove->setEnabled(false);
m_code_edit->setEnabled(false);
}
void ShaderOverrideWidget::OnRefreshClicked()
{
// Re-read overrides from the INI file and refresh the list display
LoadOverrides();
}
void ShaderOverrideWidget::OnReloadClicked()
{
// Force the runtime ShaderHunter to reload overrides from the INI,
// so enabled/disabled changes take effect in the running game without restarting.
auto& hunter = ShaderHunter::GetInstance();
hunter.LoadOverrides(m_game_id);
}
@@ -0,0 +1,54 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QPointer>
#include <QWidget>
#include <string>
#include <vector>
#include "VideoCommon/ShaderHunter.h"
class QListWidget;
class QListWidgetItem;
class QPushButton;
class ShaderHunterWidget;
class ShaderOverrideWidget : public QWidget
{
Q_OBJECT
public:
explicit ShaderOverrideWidget(std::string game_id);
~ShaderOverrideWidget() override;
private:
void CreateWidgets();
void ConnectWidgets();
void UpdateList();
void LoadOverrides();
void SaveOverrides();
void OnItemChanged(QListWidgetItem* item);
void OnSelectionChanged();
void OnAddClicked();
void OnEditClicked();
void OnRemoveClicked();
void OnRefreshClicked();
void OnReloadClicked();
std::vector<std::string> CollectAvailableFlags() const;
std::string m_game_id;
QListWidget* m_code_list;
QPushButton* m_code_add;
QPushButton* m_code_edit;
QPushButton* m_code_remove;
QPushButton* m_code_refresh;
QPushButton* m_code_reload;
QPushButton* m_shader_hunter;
QPointer<ShaderHunterWidget> m_shader_hunter_widget;
std::vector<ShaderHunter::ShaderOverride> m_overrides;
};
@@ -0,0 +1,335 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
#include <algorithm>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <QAbstractItemView>
#include <QBrush>
#include <QCheckBox>
#include <QColor>
#include <QDialogButtonBox>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QImageReader>
#include <QLabel>
#include <QPixmap>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSize>
#include <QTimer>
#include <QTreeWidget>
#include <QVBoxLayout>
#include "Common/FileUtil.h"
#include "Core/ConfigManager.h"
namespace
{
QString ToHashHex(u64 hash)
{
return QStringLiteral("%1").arg(static_cast<qulonglong>(hash), 16, 16, QLatin1Char('0'));
}
std::unordered_map<u64, QString> FindTexturePreviewPaths(const std::vector<u64>& hashes)
{
std::unordered_map<u64, QString> result;
if (hashes.empty())
return result;
std::unordered_set<u64> remaining(hashes.begin(), hashes.end());
std::unordered_map<u64, QString> patterns;
patterns.reserve(remaining.size());
for (u64 hash : remaining)
patterns.emplace(hash, QStringLiteral("_%1_").arg(ToHashHex(hash)));
const QString dump_root = QString::fromStdString(File::GetUserPath(D_DUMPTEXTURES_IDX));
QStringList roots;
const std::string game_id = SConfig::GetInstance().GetGameID();
if (!game_id.empty())
roots.push_back(QDir::cleanPath(dump_root + QString::fromStdString(game_id)));
roots.push_back(QDir::cleanPath(dump_root));
for (const QString& root : roots)
{
if (!QDir(root).exists())
continue;
QDirIterator it(root, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext() && !remaining.empty())
{
const QString path = it.next();
const QFileInfo info(path);
const QString suffix = info.suffix().toLower();
if (suffix != QStringLiteral("png") && suffix != QStringLiteral("jpg") &&
suffix != QStringLiteral("jpeg") && suffix != QStringLiteral("bmp") &&
suffix != QStringLiteral("tga"))
{
continue;
}
const QString filename = info.fileName().toLower();
for (auto iter = remaining.begin(); iter != remaining.end();)
{
const u64 hash = *iter;
if (filename.contains(patterns[hash]))
{
result.emplace(hash, path);
iter = remaining.erase(iter);
}
else
{
++iter;
}
}
}
if (remaining.empty())
break;
}
return result;
}
QPixmap LoadPreviewPixmapFresh(const QString& path)
{
QImageReader reader(path);
reader.setAutoTransform(true);
const QImage image = reader.read();
if (image.isNull())
return {};
return QPixmap::fromImage(image);
}
} // namespace
QDialog* ShowTextureHashBrowserDialog(QWidget* parent, const TextureHashBrowserConfig& config)
{
auto* dlg = new QDialog(parent);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->setWindowTitle(config.title);
dlg->setMinimumSize(620, 320);
auto* info = new QLabel;
info->setWordWrap(true);
auto* continuous_scan_check = new QCheckBox(QObject::tr("Continuous Scan"));
continuous_scan_check->setToolTip(
QObject::tr("Continuously refresh while this window is open.\n"
"Textures seen during scan stay visible in gray."));
auto* scan_timer = new QTimer(dlg);
scan_timer->setInterval(250);
auto* tree = new QTreeWidget;
tree->setHeaderLabels({QObject::tr("Use"), QObject::tr("Texture Hash"), QObject::tr("Name"),
QObject::tr("Preview")});
tree->setRootIsDecorated(false);
tree->setSelectionMode(QAbstractItemView::NoSelection);
tree->header()->setStretchLastSection(true);
tree->setIconSize(QSize(64, 64));
auto selected_hashes = std::make_shared<std::unordered_set<u64>>(
config.initial_selected_hashes.begin(), config.initial_selected_hashes.end());
auto scanned_hashes = std::make_shared<std::unordered_map<u64, std::string>>();
const auto notify_live_selection = [selected_hashes, config]() {
if (!config.live_selection_changed)
return;
std::vector<u64> hashes(selected_hashes->begin(), selected_hashes->end());
std::sort(hashes.begin(), hashes.end());
config.live_selection_changed(hashes);
};
const auto populate = [tree, info, selected_hashes, scanned_hashes, continuous_scan_check,
config, notify_live_selection]() {
notify_live_selection();
const QString current_label =
config.fetch_current_label ? config.fetch_current_label() : config.current_label;
const auto textures = config.fetch_current_entries ? config.fetch_current_entries() :
std::vector<TextureHashBrowserEntry>{};
std::unordered_map<u64, TextureHashBrowserEntry> captured;
captured.reserve(textures.size());
for (const auto& tex : textures)
{
captured.emplace(tex.hash, tex);
if (continuous_scan_check->isChecked())
{
auto& saved_name = (*scanned_hashes)[tex.hash];
if (saved_name.empty() && !tex.name.empty())
saved_name = tex.name;
}
}
std::vector<u64> texture_hashes;
texture_hashes.reserve(textures.size() + selected_hashes->size() + scanned_hashes->size());
for (const auto& [captured_hash, _] : captured)
texture_hashes.push_back(captured_hash);
for (u64 saved_hash : *selected_hashes)
texture_hashes.push_back(saved_hash);
for (const auto& [seen_hash, _] : *scanned_hashes)
texture_hashes.push_back(seen_hash);
std::sort(texture_hashes.begin(), texture_hashes.end());
texture_hashes.erase(std::unique(texture_hashes.begin(), texture_hashes.end()),
texture_hashes.end());
const auto preview_paths = FindTexturePreviewPaths(texture_hashes);
const QSignalBlocker blocker(tree);
tree->clear();
int saved_only_count = 0;
int scanned_only_count = 0;
for (u64 hash : texture_hashes)
{
const auto captured_it = captured.find(hash);
const bool in_current_scene = (captured_it != captured.end());
const bool is_saved_only = !in_current_scene && selected_hashes->count(hash) > 0;
if (is_saved_only)
++saved_only_count;
const bool is_scanned_only = !in_current_scene && !is_saved_only &&
scanned_hashes->find(hash) != scanned_hashes->end();
if (is_scanned_only)
++scanned_only_count;
auto* item = new QTreeWidgetItem;
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable);
item->setCheckState(0, selected_hashes->count(hash) > 0 ? Qt::Checked : Qt::Unchecked);
item->setText(1, ToHashHex(hash));
if (in_current_scene)
{
const auto& tex = captured_it->second;
item->setText(2, tex.name.empty() ? QObject::tr("(unknown)") :
QString::fromStdString(tex.name));
}
else
{
if (is_saved_only)
{
item->setText(2, QObject::tr("(saved filter, not in current selection)"));
}
else
{
const auto scanned_it = scanned_hashes->find(hash);
const bool has_scanned_name =
scanned_it != scanned_hashes->end() && !scanned_it->second.empty();
item->setText(2, has_scanned_name ? QString::fromStdString(scanned_it->second) :
QObject::tr("(seen during scan, not in current selection)"));
}
const QBrush gray_brush(QColor(140, 140, 140));
item->setForeground(1, gray_brush);
item->setForeground(2, gray_brush);
}
item->setData(0, Qt::UserRole, static_cast<qulonglong>(hash));
const auto preview_it = preview_paths.find(hash);
if (preview_it != preview_paths.end())
{
QPixmap pixmap = LoadPreviewPixmapFresh(preview_it->second);
if (!pixmap.isNull())
{
item->setData(3, Qt::DecorationRole,
pixmap.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
item->setSizeHint(3, QSize(72, 72));
}
}
tree->addTopLevelItem(item);
}
for (int i = 0; i < 4; ++i)
tree->resizeColumnToContents(i);
if (textures.empty())
{
if (saved_only_count > 0 || scanned_only_count > 0)
{
info->setText(QObject::tr(
"%1\nSaved filters and previously scanned textures are shown in gray.")
.arg(config.empty_info_text));
}
else
{
info->setText(config.empty_info_text);
}
}
else
{
info->setText(QObject::tr(
"%1 current texture(s) for %4. %2 saved-only and %3 scanned-only shown in gray.\n"
"Check rows and press Apply to update Texture Filters.")
.arg(textures.size())
.arg(saved_only_count)
.arg(scanned_only_count)
.arg(current_label));
}
};
QObject::connect(tree, &QTreeWidget::itemChanged, dlg,
[selected_hashes, notify_live_selection](QTreeWidgetItem* item, int column) {
if (!item || column != 0)
return;
const u64 texture_hash = item->data(0, Qt::UserRole).toULongLong();
const bool enabled = item->checkState(0) == Qt::Checked;
if (enabled)
selected_hashes->insert(texture_hash);
else
selected_hashes->erase(texture_hash);
notify_live_selection();
});
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
auto* refresh_button = buttons->addButton(QObject::tr("Refresh"), QDialogButtonBox::ActionRole);
auto* apply_button = buttons->addButton(QObject::tr("Apply"), QDialogButtonBox::AcceptRole);
QObject::connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::close);
QObject::connect(refresh_button, &QPushButton::clicked, dlg, populate);
QObject::connect(scan_timer, &QTimer::timeout, dlg, populate);
QObject::connect(continuous_scan_check, &QCheckBox::toggled, dlg, [scan_timer, populate](bool checked) {
if (checked)
{
populate();
scan_timer->start();
}
else
{
scan_timer->stop();
}
});
QObject::connect(apply_button, &QPushButton::clicked, dlg, [selected_hashes, config]() {
if (!config.apply_selected_hashes)
return;
std::vector<u64> hashes(selected_hashes->begin(), selected_hashes->end());
std::sort(hashes.begin(), hashes.end());
config.apply_selected_hashes(hashes);
});
auto* layout = new QVBoxLayout;
layout->addWidget(info);
layout->addWidget(tree);
auto* bottom_layout = new QHBoxLayout;
bottom_layout->addWidget(continuous_scan_check);
bottom_layout->addStretch();
bottom_layout->addWidget(buttons);
layout->addLayout(bottom_layout);
dlg->setLayout(layout);
populate();
QTimer::singleShot(200, dlg, populate);
dlg->show();
return dlg;
}
@@ -0,0 +1,35 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <string>
#include <vector>
#include <QDialog>
#include <QString>
#include "Common/CommonTypes.h"
class QWidget;
struct TextureHashBrowserEntry
{
u64 hash = 0;
std::string name;
};
struct TextureHashBrowserConfig
{
QString title;
QString empty_info_text;
QString current_label;
std::function<QString()> fetch_current_label;
std::vector<u64> initial_selected_hashes;
std::function<std::vector<TextureHashBrowserEntry>()> fetch_current_entries;
std::function<void(const std::vector<u64>&)> apply_selected_hashes;
std::function<void(const std::vector<u64>&)> live_selection_changed;
};
QDialog* ShowTextureHashBrowserDialog(QWidget* parent, const TextureHashBrowserConfig& config);
@@ -0,0 +1,620 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Config/VRConfigWidget.h"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
#include <utility>
#include <vector>
#include <QCheckBox>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QStyle>
#include <QTabWidget>
#include <QVBoxLayout>
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Common/StringUtil.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/ConfigManager.h"
#include "DolphinQt/Config/GameConfigEdit.h"
#include "DolphinQt/QtUtils/QtUtils.h"
namespace
{
constexpr std::string_view VR_SECTION_NAME = "Graphics.VR";
constexpr std::string_view LEGACY_VR_SECTION_NAME = "GFX.VR";
constexpr double VR_GAME_UPM_MIN = 0.01;
constexpr double VR_GAME_UPM_MAX = 1000.0;
constexpr double VR_GAME_UPM_STEP = 0.01;
void ResetTabPages(QTabWidget* tab)
{
while (tab->count() > 0)
{
QWidget* const page = tab->widget(0);
tab->removeTab(0);
delete page;
}
}
} // namespace
VRConfigWidget::VRConfigWidget(std::string game_id, QWidget* parent)
: QWidget(parent), m_game_id(std::move(game_id))
{
CreateWidgets();
LoadFromFile();
}
void VRConfigWidget::CreateWidgets()
{
auto* general_layout = new QVBoxLayout;
auto* description = new QLabel(
tr("Configure per-game VR overrides saved to GameSettingsVR/<GameID>.ini."));
description->setWordWrap(true);
general_layout->addWidget(description);
auto* override_group = new QGroupBox(tr("Overrides"));
auto* form = new QFormLayout;
override_group->setLayout(form);
auto* units_row = new QWidget;
auto* units_layout = new QHBoxLayout;
units_layout->setContentsMargins(0, 0, 0, 0);
units_row->setLayout(units_layout);
m_override_units_per_meter = new QCheckBox(tr("Override"));
m_units_per_meter = new QDoubleSpinBox;
m_units_per_meter->setRange(VR_GAME_UPM_MIN, VR_GAME_UPM_MAX);
m_units_per_meter->setSingleStep(VR_GAME_UPM_STEP);
m_units_per_meter->setDecimals(2);
m_units_per_meter->setSuffix(tr(" units/m"));
units_layout->addWidget(m_override_units_per_meter);
units_layout->addWidget(m_units_per_meter, 1);
auto* lean_back_row = new QWidget;
auto* lean_back_layout = new QHBoxLayout;
lean_back_layout->setContentsMargins(0, 0, 0, 0);
lean_back_row->setLayout(lean_back_layout);
m_override_lean_back_angle = new QCheckBox(tr("Override"));
m_lean_back_angle = new QDoubleSpinBox;
m_lean_back_angle->setRange(Config::GFX_VR_LEAN_BACK_ANGLE_MIN, Config::GFX_VR_LEAN_BACK_ANGLE_MAX);
m_lean_back_angle->setSingleStep(Config::GFX_VR_LEAN_BACK_ANGLE_STEP);
m_lean_back_angle->setDecimals(1);
m_lean_back_angle->setSuffix(tr(" deg"));
lean_back_layout->addWidget(m_override_lean_back_angle);
lean_back_layout->addWidget(m_lean_back_angle, 1);
auto* camera_forward_row = new QWidget;
auto* camera_forward_layout = new QHBoxLayout;
camera_forward_layout->setContentsMargins(0, 0, 0, 0);
camera_forward_row->setLayout(camera_forward_layout);
m_override_camera_forward = new QCheckBox(tr("Override"));
m_camera_forward = new QDoubleSpinBox;
m_camera_forward->setRange(Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX);
m_camera_forward->setSingleStep(Config::GFX_VR_CAMERA_FORWARD_STEP);
m_camera_forward->setDecimals(1);
m_camera_forward->setSuffix(tr(" m"));
camera_forward_layout->addWidget(m_override_camera_forward);
camera_forward_layout->addWidget(m_camera_forward, 1);
auto* head_locked_curvature_row = new QWidget;
auto* head_locked_curvature_layout = new QHBoxLayout;
head_locked_curvature_layout->setContentsMargins(0, 0, 0, 0);
head_locked_curvature_row->setLayout(head_locked_curvature_layout);
m_override_head_locked_curvature = new QCheckBox(tr("Override"));
m_head_locked_curvature = new QDoubleSpinBox;
m_head_locked_curvature->setRange(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX);
m_head_locked_curvature->setSingleStep(Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP);
m_head_locked_curvature->setDecimals(2);
head_locked_curvature_layout->addWidget(m_override_head_locked_curvature);
head_locked_curvature_layout->addWidget(m_head_locked_curvature, 1);
auto* element_depth_row = new QWidget;
auto* element_depth_layout = new QHBoxLayout;
element_depth_layout->setContentsMargins(0, 0, 0, 0);
element_depth_row->setLayout(element_depth_layout);
m_override_element_depth = new QCheckBox(tr("Override"));
m_element_depth = new QDoubleSpinBox;
m_element_depth->setRange(Config::GFX_VR_ELEMENT_DEPTH_MIN, Config::GFX_VR_ELEMENT_DEPTH_MAX);
m_element_depth->setSingleStep(Config::GFX_VR_ELEMENT_DEPTH_STEP);
m_element_depth->setDecimals(4);
element_depth_layout->addWidget(m_override_element_depth);
element_depth_layout->addWidget(m_element_depth, 1);
m_virtual_screen_mode = new QComboBox;
m_dont_clear_screen_mode = new QComboBox;
m_opcode_replay_mode = new QComboBox;
m_force_vbi_90hz_mode = new QComboBox;
PopulateBoolModeCombo(m_virtual_screen_mode);
PopulateBoolModeCombo(m_dont_clear_screen_mode);
PopulateReplayModeCombo(m_opcode_replay_mode);
PopulateBoolModeCombo(m_force_vbi_90hz_mode);
form->addRow(tr("Units per Meter"), units_row);
form->addRow(tr("Lean Back Angle"), lean_back_row);
form->addRow(tr("Camera Forward"), camera_forward_row);
form->addRow(tr("Head Locked Curvature"), head_locked_curvature_row);
form->addRow(tr("Element Depth"), element_depth_row);
form->addRow(tr("Virtual Screen"), m_virtual_screen_mode);
form->addRow(tr("Don't Clear Screen"), m_dont_clear_screen_mode);
form->addRow(tr("Opcode Replay"), m_opcode_replay_mode);
form->addRow(tr("Force VBI Frequency to 90 Hz in VR"), m_force_vbi_90hz_mode);
general_layout->addWidget(override_group);
general_layout->addStretch();
auto* const general_widget = new QWidget;
general_widget->setLayout(general_layout);
auto* editor_layout = new QVBoxLayout;
auto* local_group = new QGroupBox(tr("VR User Config"));
auto* local_layout = new QVBoxLayout;
m_local_tab = new QTabWidget;
local_layout->addWidget(m_local_tab);
local_group->setLayout(local_layout);
editor_layout->addWidget(local_group);
auto* const editor_widget = new QWidget;
editor_widget->setLayout(editor_layout);
auto* const tabs = new QTabWidget;
tabs->addTab(general_widget, tr("General"));
const int editor_index = tabs->addTab(editor_widget, tr("Editor"));
auto* const root_layout = new QVBoxLayout;
auto* const help_label = new QLabel(tr("These VR settings override core Dolphin settings."));
auto* const help_widget =
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxQuestion, help_label);
root_layout->addWidget(help_widget);
root_layout->addWidget(tabs);
setLayout(root_layout);
connect(m_override_units_per_meter, &QCheckBox::toggled, this, [this](bool checked) {
m_units_per_meter->setEnabled(checked);
SaveToFile();
});
connect(m_units_per_meter, &QDoubleSpinBox::valueChanged, this,
[this](double) { SaveToFile(); });
connect(m_override_lean_back_angle, &QCheckBox::toggled, this, [this](bool checked) {
m_lean_back_angle->setEnabled(checked);
SaveToFile();
});
connect(m_lean_back_angle, &QDoubleSpinBox::valueChanged, this,
[this](double) { SaveToFile(); });
connect(m_override_camera_forward, &QCheckBox::toggled, this, [this](bool checked) {
m_camera_forward->setEnabled(checked);
SaveToFile();
});
connect(m_camera_forward, &QDoubleSpinBox::valueChanged, this,
[this](double) { SaveToFile(); });
connect(m_override_head_locked_curvature, &QCheckBox::toggled, this, [this](bool checked) {
m_head_locked_curvature->setEnabled(checked);
SaveToFile();
});
connect(m_head_locked_curvature, &QDoubleSpinBox::valueChanged, this,
[this](double) { SaveToFile(); });
connect(m_override_element_depth, &QCheckBox::toggled, this, [this](bool checked) {
m_element_depth->setEnabled(checked);
SaveToFile();
});
connect(m_element_depth, &QDoubleSpinBox::valueChanged, this,
[this](double) { SaveToFile(); });
connect(m_virtual_screen_mode, &QComboBox::currentIndexChanged, this,
[this](int) { SaveToFile(); });
connect(m_dont_clear_screen_mode, &QComboBox::currentIndexChanged, this,
[this](int) { SaveToFile(); });
connect(m_opcode_replay_mode, &QComboBox::currentIndexChanged, this,
[this](int) { SaveToFile(); });
connect(m_force_vbi_90hz_mode, &QComboBox::currentIndexChanged, this,
[this](int) { SaveToFile(); });
connect(tabs, &QTabWidget::currentChanged, this, [this, editor_index](int index) {
// Sync editor content with current file after changes from General.
if (index == editor_index)
RefreshEditorTabs();
// Reload controls after raw INI edits.
if (m_prev_tab_index == editor_index)
{
LoadFromFile();
if (SConfig::GetInstance().GetGameID() == m_game_id)
SConfig::GetInstance().ReloadGameVRConfigOverrides();
}
m_prev_tab_index = index;
});
RefreshEditorTabs();
}
void VRConfigWidget::LoadFromFile()
{
const ValueMap values = ReadVRSectionValues(GetINIPath());
m_updating = true;
const auto units_it = values.find("UnitsPerMeter");
const bool units_overridden = units_it != values.end();
m_override_units_per_meter->setChecked(units_overridden);
m_units_per_meter->setEnabled(units_overridden);
double units_per_meter = Config::GetBase(Config::GFX_VR_UNITS_PER_METER);
if (units_overridden)
{
double parsed = units_per_meter;
if (TryParse(units_it->second, &parsed))
units_per_meter = parsed;
}
units_per_meter =
std::clamp(units_per_meter, VR_GAME_UPM_MIN, VR_GAME_UPM_MAX);
m_units_per_meter->setValue(units_per_meter);
const auto lean_back_it = values.find("LeanBackAngle");
const bool lean_back_overridden = lean_back_it != values.end();
m_override_lean_back_angle->setChecked(lean_back_overridden);
m_lean_back_angle->setEnabled(lean_back_overridden);
double lean_back_angle = Config::GetBase(Config::GFX_VR_LEAN_BACK_ANGLE);
if (lean_back_overridden)
{
double parsed = lean_back_angle;
if (TryParse(lean_back_it->second, &parsed))
lean_back_angle = parsed;
}
lean_back_angle =
std::clamp(lean_back_angle, static_cast<double>(Config::GFX_VR_LEAN_BACK_ANGLE_MIN),
static_cast<double>(Config::GFX_VR_LEAN_BACK_ANGLE_MAX));
m_lean_back_angle->setValue(lean_back_angle);
const auto camera_forward_it = values.find("CameraForward");
const bool camera_forward_overridden = camera_forward_it != values.end();
m_override_camera_forward->setChecked(camera_forward_overridden);
m_camera_forward->setEnabled(camera_forward_overridden);
double camera_forward = Config::GetBase(Config::GFX_VR_CAMERA_FORWARD);
if (camera_forward_overridden)
{
double parsed = camera_forward;
if (TryParse(camera_forward_it->second, &parsed))
camera_forward = parsed;
}
camera_forward =
std::clamp(camera_forward, static_cast<double>(Config::GFX_VR_CAMERA_FORWARD_MIN),
static_cast<double>(Config::GFX_VR_CAMERA_FORWARD_MAX));
m_camera_forward->setValue(camera_forward);
const auto head_locked_curvature_it = values.find("HeadLockedCurvature");
const bool head_locked_curvature_overridden = head_locked_curvature_it != values.end();
m_override_head_locked_curvature->setChecked(head_locked_curvature_overridden);
m_head_locked_curvature->setEnabled(head_locked_curvature_overridden);
double head_locked_curvature = Config::GetBase(Config::GFX_VR_HEAD_LOCKED_CURVATURE);
if (head_locked_curvature_overridden)
{
double parsed = head_locked_curvature;
if (TryParse(head_locked_curvature_it->second, &parsed))
head_locked_curvature = parsed;
}
head_locked_curvature =
std::clamp(head_locked_curvature, static_cast<double>(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN),
static_cast<double>(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX));
m_head_locked_curvature->setValue(head_locked_curvature);
const auto element_depth_it = values.find("ElementDepth");
const bool element_depth_overridden = element_depth_it != values.end();
m_override_element_depth->setChecked(element_depth_overridden);
m_element_depth->setEnabled(element_depth_overridden);
double element_depth = Config::GetBase(Config::GFX_VR_ELEMENT_DEPTH);
if (element_depth_overridden)
{
double parsed = element_depth;
if (TryParse(element_depth_it->second, &parsed))
element_depth = parsed;
}
element_depth =
std::clamp(element_depth, static_cast<double>(Config::GFX_VR_ELEMENT_DEPTH_MIN),
static_cast<double>(Config::GFX_VR_ELEMENT_DEPTH_MAX));
m_element_depth->setValue(element_depth);
SetBoolMode(m_virtual_screen_mode, ParseBoolMode(values, "VirtualScreen"));
SetBoolMode(m_dont_clear_screen_mode, ParseBoolMode(values, "DontClearScreen"));
SetReplayMode(m_opcode_replay_mode, ParseReplayMode(values, "OpcodeReplay"));
SetBoolMode(m_force_vbi_90hz_mode, ParseBoolMode(values, "AutoVBIFromHMD"));
m_updating = false;
}
void VRConfigWidget::SaveToFile()
{
if (m_updating || m_game_id.empty())
return;
std::vector<std::pair<std::string, std::string>> entries;
if (m_override_units_per_meter->isChecked())
{
entries.emplace_back("UnitsPerMeter",
QString::number(m_units_per_meter->value(), 'f', 2).toStdString());
}
if (m_override_lean_back_angle->isChecked())
{
entries.emplace_back("LeanBackAngle",
QString::number(m_lean_back_angle->value(), 'f', 1).toStdString());
}
if (m_override_camera_forward->isChecked())
{
entries.emplace_back("CameraForward",
QString::number(m_camera_forward->value(), 'f', 1).toStdString());
}
if (m_override_head_locked_curvature->isChecked())
{
entries.emplace_back("HeadLockedCurvature",
QString::number(m_head_locked_curvature->value(), 'f', 2).toStdString());
}
if (m_override_element_depth->isChecked())
{
entries.emplace_back("ElementDepth",
QString::number(m_element_depth->value(), 'f', 4).toStdString());
}
const auto append_bool = [&entries](const char* key, BoolMode mode) {
if (mode == BoolMode::Inherit)
return;
entries.emplace_back(key, mode == BoolMode::Enabled ? "True" : "False");
};
append_bool("VirtualScreen", GetBoolMode(m_virtual_screen_mode));
append_bool("DontClearScreen", GetBoolMode(m_dont_clear_screen_mode));
switch (GetReplayMode(m_opcode_replay_mode))
{
case ReplayMode::Off:
entries.emplace_back("OpcodeReplay", "0");
break;
case ReplayMode::Replay60To90:
entries.emplace_back("OpcodeReplay", "1");
break;
case ReplayMode::Replay30To90:
entries.emplace_back("OpcodeReplay", "2");
break;
case ReplayMode::Inherit:
break;
}
append_bool("AutoVBIFromHMD", GetBoolMode(m_force_vbi_90hz_mode));
const std::string path = GetINIPath();
std::string base = ReadFileWithoutVRSection(path);
while (!base.empty() &&
(base.back() == '\n' || base.back() == '\r' ||
std::isspace(static_cast<unsigned char>(base.back()))))
base.pop_back();
std::ostringstream out;
if (!base.empty())
out << base << "\n";
if (!entries.empty())
{
out << '[' << VR_SECTION_NAME << "]\n";
for (const auto& [key, value] : entries)
out << key << " = " << value << "\n";
}
const std::string output = out.str();
if (output.empty())
{
File::Delete(path, File::IfAbsentBehavior::NoConsoleWarning);
}
else
{
File::CreateFullPath(path);
std::ofstream file(path, std::ios::trunc);
file << output;
}
if (SConfig::GetInstance().GetGameID() == m_game_id)
SConfig::GetInstance().ReloadGameVRConfigOverrides();
}
void VRConfigWidget::RefreshEditorTabs()
{
if (!m_local_tab || m_game_id.empty())
return;
ResetTabPages(m_local_tab);
const std::string local_path = GetINIPath();
m_local_tab->addTab(new GameConfigEdit(nullptr, QString::fromStdString(local_path), false),
QString::fromStdString(m_game_id + ".ini"));
}
std::string VRConfigWidget::GetINIPath() const
{
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + m_game_id + ".ini";
}
bool VRConfigWidget::IsVRSectionHeader(std::string_view line)
{
const std::string_view trimmed = StripWhitespace(line);
if (trimmed.size() < 3 || trimmed.front() != '[' || trimmed.back() != ']')
return false;
const std::string_view section_name = trimmed.substr(1, trimmed.size() - 2);
return Common::CaseInsensitiveEquals(section_name, VR_SECTION_NAME) ||
Common::CaseInsensitiveEquals(section_name, LEGACY_VR_SECTION_NAME);
}
std::string VRConfigWidget::ReadFileWithoutVRSection(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))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
const std::string_view trimmed = StripWhitespace(line);
if (IsVRSectionHeader(trimmed))
{
skipping = true;
continue;
}
if (skipping && !trimmed.empty() && trimmed.front() == '[')
skipping = false;
if (!skipping)
out << line << "\n";
}
return out.str();
}
VRConfigWidget::ValueMap VRConfigWidget::ReadVRSectionValues(const std::string& path)
{
ValueMap values;
std::ifstream file(path);
if (!file.is_open())
return values;
bool in_section = false;
std::string line;
while (std::getline(file, line))
{
if (!line.empty() && line.back() == '\r')
line.pop_back();
const std::string_view trimmed = StripWhitespace(line);
if (trimmed.empty())
continue;
if (IsVRSectionHeader(trimmed))
{
in_section = true;
continue;
}
if (in_section && trimmed.front() == '[')
break;
if (!in_section)
continue;
std::string key;
std::string value;
Common::IniFile::ParseLine(trimmed, &key, &value);
if (!key.empty())
values.insert_or_assign(std::move(key), std::move(value));
}
return values;
}
VRConfigWidget::BoolMode VRConfigWidget::ParseBoolMode(const ValueMap& values, const char* key)
{
const auto it = values.find(key);
if (it == values.end())
return BoolMode::Inherit;
bool parsed = false;
if (TryParse(it->second, &parsed))
return parsed ? BoolMode::Enabled : BoolMode::Disabled;
return BoolMode::Inherit;
}
VRConfigWidget::ReplayMode VRConfigWidget::ParseReplayMode(const ValueMap& values, const char* key)
{
const auto it = values.find(key);
if (it == values.end())
return ReplayMode::Inherit;
int parsed = 0;
if (!TryParse(it->second, &parsed))
return ReplayMode::Inherit;
switch (parsed)
{
case 0:
return ReplayMode::Off;
case 1:
return ReplayMode::Replay60To90;
case 2:
return ReplayMode::Replay30To90;
default:
return ReplayMode::Inherit;
}
}
void VRConfigWidget::SetBoolMode(QComboBox* combo, BoolMode mode)
{
combo->setCurrentIndex(static_cast<int>(mode));
}
VRConfigWidget::BoolMode VRConfigWidget::GetBoolMode(const QComboBox* combo)
{
const int index = combo->currentIndex();
if (index == static_cast<int>(BoolMode::Enabled))
return BoolMode::Enabled;
if (index == static_cast<int>(BoolMode::Disabled))
return BoolMode::Disabled;
return BoolMode::Inherit;
}
void VRConfigWidget::PopulateBoolModeCombo(QComboBox* combo)
{
combo->addItem(QObject::tr("Inherit"));
combo->addItem(QObject::tr("Enabled"));
combo->addItem(QObject::tr("Disabled"));
}
void VRConfigWidget::SetReplayMode(QComboBox* combo, ReplayMode mode)
{
combo->setCurrentIndex(static_cast<int>(mode) + 1);
}
VRConfigWidget::ReplayMode VRConfigWidget::GetReplayMode(const QComboBox* combo)
{
switch (combo->currentIndex() - 1)
{
case 0:
return ReplayMode::Off;
case 1:
return ReplayMode::Replay60To90;
case 2:
return ReplayMode::Replay30To90;
default:
return ReplayMode::Inherit;
}
}
void VRConfigWidget::PopulateReplayModeCombo(QComboBox* combo)
{
combo->addItem(QObject::tr("Inherit"));
combo->addItem(QObject::tr("Off"));
combo->addItem(QObject::tr("60 to 90"));
combo->addItem(QObject::tr("30 to 90"));
}
@@ -0,0 +1,81 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
#include <string>
#include <QWidget>
#include "Common/StringUtil.h"
class QCheckBox;
class QComboBox;
class QDoubleSpinBox;
class QTabWidget;
class VRConfigWidget final : public QWidget
{
Q_OBJECT
public:
explicit VRConfigWidget(std::string game_id, QWidget* parent = nullptr);
private:
enum class BoolMode
{
Inherit = 0,
Enabled = 1,
Disabled = 2,
};
enum class ReplayMode
{
Inherit = -1,
Off = 0,
Replay60To90 = 1,
Replay30To90 = 2,
};
using ValueMap = std::map<std::string, std::string, Common::CaseInsensitiveLess>;
void CreateWidgets();
void LoadFromFile();
void SaveToFile();
void RefreshEditorTabs();
std::string GetINIPath() const;
static bool IsVRSectionHeader(std::string_view line);
static std::string ReadFileWithoutVRSection(const std::string& path);
static ValueMap ReadVRSectionValues(const std::string& path);
static BoolMode ParseBoolMode(const ValueMap& values, const char* key);
static ReplayMode ParseReplayMode(const ValueMap& values, const char* key);
static void SetBoolMode(QComboBox* combo, BoolMode mode);
static BoolMode GetBoolMode(const QComboBox* combo);
static void PopulateBoolModeCombo(QComboBox* combo);
static void SetReplayMode(QComboBox* combo, ReplayMode mode);
static ReplayMode GetReplayMode(const QComboBox* combo);
static void PopulateReplayModeCombo(QComboBox* combo);
const std::string m_game_id;
bool m_updating = false;
QCheckBox* m_override_units_per_meter = nullptr;
QDoubleSpinBox* m_units_per_meter = nullptr;
QCheckBox* m_override_lean_back_angle = nullptr;
QDoubleSpinBox* m_lean_back_angle = nullptr;
QCheckBox* m_override_camera_forward = nullptr;
QDoubleSpinBox* m_camera_forward = nullptr;
QCheckBox* m_override_head_locked_curvature = nullptr;
QDoubleSpinBox* m_head_locked_curvature = nullptr;
QCheckBox* m_override_element_depth = nullptr;
QDoubleSpinBox* m_element_depth = nullptr;
QComboBox* m_virtual_screen_mode = nullptr;
QComboBox* m_dont_clear_screen_mode = nullptr;
QComboBox* m_opcode_replay_mode = nullptr;
QComboBox* m_force_vbi_90hz_mode = nullptr;
QTabWidget* m_local_tab = nullptr;
int m_prev_tab_index = 0;
};
@@ -46,6 +46,23 @@
#include "Core/HW//WiimoteReal/IOWin.h"
#endif
namespace
{
WiimoteSource GetSourceFromCombo(const QComboBox* combo)
{
const QVariant value = combo->currentData();
if (!value.isValid())
return WiimoteSource::None;
return static_cast<WiimoteSource>(value.toInt());
}
bool IsConfigurableWiimoteSource(WiimoteSource source)
{
return source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR;
}
} // namespace
WiimoteControllersWidget::WiimoteControllersWidget(QWidget* parent) : QWidget(parent)
{
CreateLayout();
@@ -261,8 +278,10 @@ void WiimoteControllersWidget::CreateLayout()
auto* wm_box = m_wiimote_boxes[i] = new QComboBox();
auto* wm_button = m_wiimote_buttons[i] = new NonDefaultQPushButton(tr("Configure"));
for (const auto& item : {tr("None"), tr("Emulated Wii Remote"), tr("Real Wii Remote")})
wm_box->addItem(item);
wm_box->addItem(tr("None"), static_cast<int>(WiimoteSource::None));
wm_box->addItem(tr("Emulated Wii Remote"), static_cast<int>(WiimoteSource::Emulated));
wm_box->addItem(tr("OpenXR Wii Remote"), static_cast<int>(WiimoteSource::OpenXR));
wm_box->addItem(tr("Real Wii Remote"), static_cast<int>(WiimoteSource::Real));
int wm_row = m_wiimote_layout->rowCount();
m_wiimote_layout->addWidget(wm_label, wm_row, 1);
@@ -417,12 +436,13 @@ void WiimoteControllersWidget::OnWiimoteRefreshPressed()
void WiimoteControllersWidget::OnWiimoteConfigure(size_t index)
{
MappingWindow::Type type;
switch (m_wiimote_boxes[index]->currentIndex())
switch (GetSourceFromCombo(m_wiimote_boxes[index]))
{
case 0: // None
case 2: // Real Wii Remote
case WiimoteSource::None:
case WiimoteSource::Real:
return;
case 1: // Emulated Wii Remote
case WiimoteSource::Emulated:
case WiimoteSource::OpenXR:
type = MappingWindow::Type::MAPPING_WIIMOTE_EMU;
break;
default:
@@ -452,8 +472,14 @@ void WiimoteControllersWidget::LoadSettings(Core::State state)
{
for (size_t i = 0; i < m_wiimote_groups.size(); i++)
{
SignalBlocking(m_wiimote_boxes[i])
->setCurrentIndex(int(Config::Get(Config::GetInfoForWiimoteSource(int(i)))));
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(int(i)));
int source_index = m_wiimote_boxes[i]->findData(static_cast<int>(source));
if (source_index < 0)
source_index = m_wiimote_boxes[i]->findData(static_cast<int>(WiimoteSource::None));
if (source_index < 0)
source_index = 0;
SignalBlocking(m_wiimote_boxes[i])->setCurrentIndex(source_index);
}
SignalBlocking(m_wiimote_real_balance_board)
->setChecked(Config::Get(Config::WIIMOTE_BB_SOURCE) == WiimoteSource::Real);
@@ -497,8 +523,8 @@ void WiimoteControllersWidget::LoadSettings(Core::State state)
m_wiimote_labels[i]->setEnabled(enable_emu_bt);
m_wiimote_boxes[i]->setEnabled(enable_emu_bt && !running_netplay);
const bool is_emu_wiimote = m_wiimote_boxes[i]->currentIndex() == 1;
m_wiimote_buttons[i]->setEnabled(enable_emu_bt && is_emu_wiimote &&
const WiimoteSource source = GetSourceFromCombo(m_wiimote_boxes[i]);
m_wiimote_buttons[i]->setEnabled(enable_emu_bt && IsConfigurableWiimoteSource(source) &&
static_cast<int>(i) < num_local_wiimotes);
}
@@ -531,8 +557,8 @@ void WiimoteControllersWidget::SaveSettings()
for (size_t i = 0; i < m_wiimote_groups.size(); i++)
{
const int index = m_wiimote_boxes[i]->currentIndex();
Config::SetBaseOrCurrent(Config::GetInfoForWiimoteSource(int(i)), WiimoteSource(index));
const WiimoteSource source = GetSourceFromCombo(m_wiimote_boxes[i]);
Config::SetBaseOrCurrent(Config::GetInfoForWiimoteSource(int(i)), source);
}
}
@@ -0,0 +1,767 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/CullingCodeFinderWidget.h"
#include <algorithm>
#include <QCheckBox>
#include <QClipboard>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFileDialog>
#include <QFileInfo>
#include <QGroupBox>
#include <QGuiApplication>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QTableWidget>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <filesystem>
#include <fmt/format.h>
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Core/ActionReplay.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/PowerPC/PPCSymbolDB.h"
#include "Core/System.h"
#include "DolphinQt/Host.h"
#include "VideoCommon/CullingCodeFinder.h"
namespace
{
u32 ParseOptionalHexField(const QString& text, bool* ok)
{
QString parsed_text = text.trimmed();
if (parsed_text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
parsed_text.remove(0, 2);
*ok = parsed_text.isEmpty();
return parsed_text.isEmpty() ? 0u : parsed_text.toUInt(ok, 16);
}
QString PathLeaf(QString path)
{
return QFileInfo(path).fileName();
}
QString PathLeaf(const std::string& path)
{
return QFileInfo(QString::fromStdString(path)).fileName();
}
} // namespace
CullingCodeFinderWidget::CullingCodeFinderWidget(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("Culling Code Finder"));
setMinimumWidth(840);
CreateWidgets();
ConnectSignals();
RefreshStaticInfo();
UpdateButtonStates();
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, &CullingCodeFinderWidget::UpdateDisplay);
m_update_timer->start(16);
}
void CullingCodeFinderWidget::closeEvent(QCloseEvent* event)
{
auto& finder = CullingCodeFinder::GetInstance();
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning ||
finder.GetState() == CullingCodeFinder::ScanState::Paused)
{
finder.StopScan();
finder.AdvanceScan(Core::System::GetInstance());
}
m_update_timer->stop();
event->accept();
}
void CullingCodeFinderWidget::CreateWidgets()
{
auto* main_layout = new QVBoxLayout(this);
auto* stats_group = new QGroupBox(tr("Live Statistics"));
auto* stats_layout = new QHBoxLayout;
stats_group->setLayout(stats_layout);
stats_layout->addWidget(new QLabel(tr("Draw Calls:")));
m_live_draw_calls = new QLabel(QStringLiteral("0"));
stats_layout->addWidget(m_live_draw_calls);
stats_layout->addWidget(new QLabel(tr("Triangles:")));
m_live_triangles = new QLabel(QStringLiteral("0"));
stats_layout->addWidget(m_live_triangles);
stats_layout->addWidget(new QLabel(tr("Prims:")));
m_live_prims = new QLabel(QStringLiteral("0"));
stats_layout->addWidget(m_live_prims);
stats_layout->addWidget(new QLabel(tr("Objects:")));
m_live_objects = new QLabel(QStringLiteral("0"));
stats_layout->addWidget(m_live_objects);
stats_layout->addStretch();
main_layout->addWidget(stats_group);
auto* baseline_group = new QGroupBox(tr("Baseline"));
auto* baseline_layout = new QHBoxLayout;
baseline_group->setLayout(baseline_layout);
m_baseline_label = new QLabel(tr("Not set"));
baseline_layout->addWidget(m_baseline_label);
baseline_layout->addStretch();
m_set_baseline_btn = new QPushButton(tr("Set Baseline"));
baseline_layout->addWidget(m_set_baseline_btn);
main_layout->addWidget(baseline_group);
auto* prerequisite_group = new QGroupBox(tr("Prerequisites"));
auto* prerequisite_layout = new QVBoxLayout;
prerequisite_group->setLayout(prerequisite_layout);
auto* map_layout = new QHBoxLayout;
map_layout->addWidget(new QLabel(tr("Map File:")));
m_map_path_label = new QLabel(tr("Not found"));
m_map_path_label->setTextInteractionFlags(Qt::TextSelectableByMouse);
map_layout->addWidget(m_map_path_label, 1);
m_generate_map_btn = new QPushButton(tr("Generate .map"));
map_layout->addWidget(m_generate_map_btn);
prerequisite_layout->addLayout(map_layout);
auto* output_layout = new QHBoxLayout;
output_layout->addWidget(new QLabel(tr("Output Folder:")));
m_output_folder_edit = new QLineEdit;
output_layout->addWidget(m_output_folder_edit, 1);
m_browse_output_btn = new QPushButton(tr("Browse"));
m_open_output_btn = new QPushButton(tr("Open"));
output_layout->addWidget(m_browse_output_btn);
output_layout->addWidget(m_open_output_btn);
prerequisite_layout->addLayout(output_layout);
main_layout->addWidget(prerequisite_group);
auto* config_group = new QGroupBox(tr("Scan Configuration"));
auto* config_layout = new QVBoxLayout;
config_group->setLayout(config_layout);
auto* top_row = new QHBoxLayout;
top_row->addWidget(new QLabel(tr("Savestate Slot:")));
m_savestate_slot_spin = new QSpinBox;
m_savestate_slot_spin->setRange(1, 10);
m_savestate_slot_spin->setValue(1);
top_row->addWidget(m_savestate_slot_spin);
top_row->addWidget(new QLabel(tr("Settle Frames:")));
m_settle_frames_spin = new QSpinBox;
m_settle_frames_spin->setRange(1, 60);
m_settle_frames_spin->setValue(5);
top_row->addWidget(m_settle_frames_spin);
top_row->addWidget(new QLabel(tr("Candidate Limit:")));
m_candidate_limit_spin = new QSpinBox;
m_candidate_limit_spin->setRange(0, 1000000);
m_candidate_limit_spin->setValue(0);
m_candidate_limit_spin->setSpecialValueText(tr("Unlimited"));
top_row->addWidget(m_candidate_limit_spin);
top_row->addStretch();
config_layout->addLayout(top_row);
m_settle_frames_help = new QLabel(
tr("Settle Frames are emulated frames after each savestate reload. Turbo only reduces wall-"
"clock scan time."));
m_settle_frames_help->setWordWrap(true);
config_layout->addWidget(m_settle_frames_help);
auto* threshold_row = new QHBoxLayout;
threshold_row->addWidget(new QLabel(tr("Min Draw Calls +:")));
m_min_draw_call_spin = new QSpinBox;
m_min_draw_call_spin->setRange(1, 1000000);
m_min_draw_call_spin->setValue(1);
m_min_draw_call_spin->setToolTip(
tr("Minimum increase in draw calls versus the baseline required to keep a result."));
threshold_row->addWidget(m_min_draw_call_spin);
threshold_row->addWidget(new QLabel(tr("Min Triangles +:")));
m_min_triangle_spin = new QSpinBox;
m_min_triangle_spin->setRange(0, 100000000);
m_min_triangle_spin->setValue(0);
m_min_triangle_spin->setToolTip(
tr("Minimum increase in drawn triangles versus the baseline required to keep a result."));
threshold_row->addWidget(m_min_triangle_spin);
threshold_row->addWidget(new QLabel(tr("Min Objects +:")));
m_min_object_spin = new QSpinBox;
m_min_object_spin->setRange(0, 1000000);
m_min_object_spin->setValue(0);
m_min_object_spin->setToolTip(
tr("Minimum increase in drawn objects versus the baseline required to keep a result."));
threshold_row->addWidget(m_min_object_spin);
threshold_row->addStretch();
config_layout->addLayout(threshold_row);
auto* filter_row = new QHBoxLayout;
filter_row->addWidget(new QLabel(tr("Start Address:")));
m_start_address_edit = new QLineEdit;
m_start_address_edit->setPlaceholderText(QStringLiteral("0 or 80052FE8"));
filter_row->addWidget(m_start_address_edit);
filter_row->addWidget(new QLabel(tr("End Address:")));
m_end_address_edit = new QLineEdit;
m_end_address_edit->setPlaceholderText(QStringLiteral("0 or 80053000"));
filter_row->addWidget(m_end_address_edit);
filter_row->addWidget(new QLabel(tr("Symbol Filter:")));
m_symbol_filter_edit = new QLineEdit;
filter_row->addWidget(m_symbol_filter_edit, 1);
config_layout->addLayout(filter_row);
auto* runtime_row = new QHBoxLayout;
m_turbo_during_scan_check = new QCheckBox(tr("Turbo During Scan"));
m_turbo_during_scan_check->setChecked(true);
runtime_row->addWidget(m_turbo_during_scan_check);
m_mute_audio_during_scan_check = new QCheckBox(tr("Mute Audio During Scan"));
m_mute_audio_during_scan_check->setChecked(true);
runtime_row->addWidget(m_mute_audio_during_scan_check);
m_auto_capture_reference_state_check = new QCheckBox(tr("Auto-Capture Reference State"));
m_auto_capture_reference_state_check->setChecked(true);
runtime_row->addWidget(m_auto_capture_reference_state_check);
runtime_row->addStretch();
config_layout->addLayout(runtime_row);
auto* template_row = new QHBoxLayout;
constexpr std::array<const char*, 4> labels = {"38600001 + BLR", "38600000 + BLR",
"60000001 + BLR", "60000000 + BLR"};
for (size_t i = 0; i < labels.size(); ++i)
{
m_template_checks[i] = new QCheckBox(QString::fromLatin1(labels[i]));
m_template_checks[i]->setChecked(true);
template_row->addWidget(m_template_checks[i]);
}
template_row->addStretch();
config_layout->addLayout(template_row);
main_layout->addWidget(config_group);
auto* controls_layout = new QHBoxLayout;
m_start_btn = new QPushButton(tr("Start Scan"));
m_restore_btn = new QPushButton(tr("Restore Session"));
m_pause_btn = new QPushButton(tr("Pause"));
m_stop_btn = new QPushButton(tr("Stop"));
controls_layout->addWidget(m_start_btn);
controls_layout->addWidget(m_restore_btn);
controls_layout->addWidget(m_pause_btn);
controls_layout->addWidget(m_stop_btn);
controls_layout->addStretch();
main_layout->addLayout(controls_layout);
m_progress_bar = new QProgressBar;
m_progress_bar->setRange(0, 100);
m_progress_bar->setValue(0);
main_layout->addWidget(m_progress_bar);
m_status_label = new QLabel(tr("Idle"));
main_layout->addWidget(m_status_label);
auto* results_group = new QGroupBox(tr("Results"));
auto* results_layout = new QVBoxLayout;
results_group->setLayout(results_layout);
m_results_table = new QTableWidget(0, 8);
m_results_table->setHorizontalHeaderLabels(
{tr("Symbol"), tr("Address"), tr("Template"), tr("DC Delta"), tr("Tri Delta"),
tr("Obj Delta"), tr("Timeout"), tr("Screenshot")});
m_results_table->setSelectionBehavior(QTableWidget::SelectRows);
m_results_table->setSelectionMode(QTableWidget::ExtendedSelection);
m_results_table->setEditTriggers(QTableWidget::NoEditTriggers);
m_results_table->horizontalHeader()->setStretchLastSection(true);
m_results_table->verticalHeader()->setVisible(false);
results_layout->addWidget(m_results_table);
m_show_timeout_results_check = new QCheckBox(tr("Show Timeout Results"));
m_show_timeout_results_check->setChecked(false);
results_layout->addWidget(m_show_timeout_results_check);
auto* result_buttons = new QHBoxLayout;
m_generate_ar_btn = new QPushButton(tr("Generate AR Code"));
m_reset_results_btn = new QPushButton(tr("Reset Results"));
result_buttons->addWidget(m_generate_ar_btn);
result_buttons->addWidget(m_reset_results_btn);
result_buttons->addStretch();
results_layout->addLayout(result_buttons);
main_layout->addWidget(results_group);
}
void CullingCodeFinderWidget::ConnectSignals()
{
connect(m_set_baseline_btn, &QPushButton::clicked, this,
&CullingCodeFinderWidget::OnSetBaseline);
connect(m_generate_map_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnGenerateMap);
connect(m_start_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStartScan);
connect(m_restore_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnRestoreSession);
connect(m_pause_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnPauseScan);
connect(m_stop_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStopScan);
connect(m_generate_ar_btn, &QPushButton::clicked, this,
&CullingCodeFinderWidget::OnGenerateARCode);
connect(m_reset_results_btn, &QPushButton::clicked, this,
&CullingCodeFinderWidget::OnResetResults);
connect(m_show_timeout_results_check, &QCheckBox::toggled, this, [this] {
UpdateResultsTable();
UpdateButtonStates();
});
connect(m_browse_output_btn, &QPushButton::clicked, this,
&CullingCodeFinderWidget::OnBrowseOutputFolder);
connect(m_open_output_btn, &QPushButton::clicked, this,
&CullingCodeFinderWidget::OnOpenOutputFolder);
}
void CullingCodeFinderWidget::RefreshStaticInfo()
{
const std::string game_id = SConfig::GetInstance().GetGameID();
std::string map_path;
PPCSymbolDB::FindMapFile(&map_path, nullptr);
if (map_path.empty())
{
const std::string finder_map_path = CullingCodeFinder::GetInstance().GetMapPath();
const std::string expected_suffix = game_id + ".map";
if (!finder_map_path.empty() && !game_id.empty() && finder_map_path.ends_with(expected_suffix))
map_path = finder_map_path;
}
m_map_path_label->setText(map_path.empty() ? tr("Not found") : QString::fromStdString(map_path));
if (m_output_folder_edit->text().isEmpty() && !game_id.empty())
{
m_output_folder_edit->setText(
QString::fromStdString(CullingCodeFinder::GetDefaultOutputFolder(game_id)));
}
}
void CullingCodeFinderWidget::UpdateDisplay()
{
auto& finder = CullingCodeFinder::GetInstance();
const auto stats = finder.GetLatestStats();
m_live_draw_calls->setText(QString::number(stats.draw_calls));
m_live_triangles->setText(QString::number(stats.triangles_drawn));
m_live_prims->setText(QString::number(stats.prims));
m_live_objects->setText(QString::number(stats.drawn_objects));
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
finder.AdvanceScan(Core::System::GetInstance());
const auto baseline = finder.GetBaseline();
m_baseline_label->setText(finder.HasBaseline() ?
tr("DC: %1 | Tri: %2 | Prims: %3 | Obj: %4")
.arg(baseline.draw_calls)
.arg(baseline.triangles_drawn)
.arg(baseline.prims)
.arg(baseline.drawn_objects) :
tr("Not set"));
m_progress_bar->setValue(static_cast<int>(finder.GetProgress() * 100.0f));
m_status_label->setText(QString::fromStdString(finder.GetStatusText()));
const int result_count = finder.GetResultCount();
if (result_count != m_last_result_count)
{
UpdateResultsTable();
m_last_result_count = result_count;
}
RefreshStaticInfo();
UpdateButtonStates();
}
void CullingCodeFinderWidget::OnSetBaseline()
{
auto& finder = CullingCodeFinder::GetInstance();
finder.SetBaseline();
UpdateDisplay();
}
void CullingCodeFinderWidget::OnStartScan()
{
CullingCodeFinder::ScanConfig config;
bool start_address_ok = false;
bool end_address_ok = false;
const u32 parsed_start_address =
ParseOptionalHexField(m_start_address_edit->text(), &start_address_ok);
const u32 parsed_end_address = ParseOptionalHexField(m_end_address_edit->text(), &end_address_ok);
if (!start_address_ok)
{
QMessageBox::warning(this, tr("Invalid Start Address"),
tr("Enter the start address as hexadecimal, for example 80052FE8."));
return;
}
if (!end_address_ok)
{
QMessageBox::warning(this, tr("Invalid End Address"),
tr("Enter the end address as hexadecimal, for example 80053000."));
return;
}
if (parsed_start_address != 0 && parsed_end_address != 0 &&
parsed_start_address > parsed_end_address)
{
QMessageBox::warning(this, tr("Invalid Address Range"),
tr("The start address must be lower than or equal to the end address."));
return;
}
config.savestate_slot = m_savestate_slot_spin->value();
config.settle_frames = m_settle_frames_spin->value();
config.min_draw_call_increase = m_min_draw_call_spin->value();
config.min_triangle_increase = m_min_triangle_spin->value();
config.min_object_increase = m_min_object_spin->value();
config.candidate_limit = m_candidate_limit_spin->value();
config.start_address = parsed_start_address;
config.end_address = parsed_end_address;
config.turbo_during_scan = m_turbo_during_scan_check->isChecked();
config.mute_audio_during_scan = m_mute_audio_during_scan_check->isChecked();
config.auto_capture_reference_state = m_auto_capture_reference_state_check->isChecked();
config.symbol_filter = m_symbol_filter_edit->text().trimmed().toStdString();
config.output_folder = m_output_folder_edit->text().trimmed().toStdString();
for (size_t i = 0; i < m_template_checks.size(); ++i)
config.enabled_templates[i] = m_template_checks[i]->isChecked();
std::string error;
if (!CullingCodeFinder::GetInstance().StartScan(Core::System::GetInstance(), std::move(config),
&error))
{
QMessageBox::warning(this, tr("Unable to Start Scan"), QString::fromStdString(error));
return;
}
m_last_result_count = -1;
UpdateDisplay();
}
void CullingCodeFinderWidget::OnRestoreSession()
{
const QString output_folder = m_output_folder_edit->text().trimmed();
if (output_folder.isEmpty())
{
QMessageBox::warning(this, tr("No Output Folder"),
tr("Select the scan output folder that contains manifest.ini first."));
return;
}
std::string error;
std::string restored_message;
auto& finder = CullingCodeFinder::GetInstance();
if (!finder.RestoreSession(Core::System::GetInstance(), output_folder.toStdString(), &error,
&restored_message))
{
QMessageBox::warning(this, tr("Unable to Restore Session"), QString::fromStdString(error));
return;
}
ApplyConfigToWidgets();
m_last_result_count = -1;
finder.AdvanceScan(Core::System::GetInstance());
UpdateDisplay();
QMessageBox::information(this, tr("Session Restored"),
QString::fromStdString(restored_message));
}
void CullingCodeFinderWidget::OnGenerateMap()
{
std::string error;
if (!CullingCodeFinder::GetInstance().GenerateSymbolMap(Core::System::GetInstance(), &error))
{
QMessageBox::warning(this, tr("Unable to Generate Map"), QString::fromStdString(error));
return;
}
emit Host::GetInstance()->PPCSymbolsChanged();
RefreshStaticInfo();
QMessageBox::information(this, tr("Map Generated"),
tr("Rebuilt and saved the writable symbol map for the running game."));
}
void CullingCodeFinderWidget::OnPauseScan()
{
auto& finder = CullingCodeFinder::GetInstance();
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
{
finder.PauseScan();
finder.AdvanceScan(Core::System::GetInstance());
}
else if (finder.GetState() == CullingCodeFinder::ScanState::Paused)
{
finder.ResumeScan();
}
UpdateDisplay();
}
void CullingCodeFinderWidget::OnStopScan()
{
auto& finder = CullingCodeFinder::GetInstance();
finder.StopScan();
finder.AdvanceScan(Core::System::GetInstance());
UpdateDisplay();
}
void CullingCodeFinderWidget::OnGenerateARCode()
{
const std::string game_id = SConfig::GetInstance().GetGameID();
if (game_id.empty())
{
QMessageBox::warning(this, tr("No Game Running"),
tr("Boot a game before generating Action Replay codes."));
return;
}
const QModelIndexList selected_rows = m_results_table->selectionModel()->selectedRows();
if (selected_rows.empty())
{
QMessageBox::information(this, tr("No Results Selected"),
tr("Select one or more results first."));
return;
}
const auto results = CullingCodeFinder::GetInstance().GetResults();
std::vector<int> row_indices;
row_indices.reserve(selected_rows.size());
for (const QModelIndex& index : selected_rows)
row_indices.push_back(index.row());
std::ranges::sort(row_indices);
std::vector<ActionReplay::ARCode> generated_codes;
generated_codes.reserve(row_indices.size());
QString clipboard_text;
for (int row : row_indices)
{
if (row < 0 || row >= static_cast<int>(m_visible_result_indices.size()))
continue;
const int result_index = m_visible_result_indices[static_cast<size_t>(row)];
if (result_index < 0 || result_index >= static_cast<int>(results.size()))
continue;
const auto& result = results[static_cast<size_t>(result_index)];
const auto lines = CullingCodeFinder::BuildARCodeLines(result.address, result.template_kind);
ActionReplay::ARCode code;
code.name =
fmt::format("VR Culling - {} [{:08X}] {}", result.symbol_name, result.address,
CullingCodeFinder::GetPatchTemplateName(result.template_kind));
code.enabled = true;
code.default_enabled = false;
code.user_defined = true;
for (const std::string& line : lines)
{
const auto parsed = ActionReplay::DeserializeLine(line);
if (const auto* entry = std::get_if<ActionReplay::AREntry>(&parsed))
code.ops.push_back(*entry);
}
if (code.ops.size() != lines.size())
continue;
generated_codes.push_back(std::move(code));
if (!clipboard_text.isEmpty())
clipboard_text.append(QStringLiteral("\n\n"));
clipboard_text.append(QString::fromStdString(result.ar_code_text));
}
if (generated_codes.empty())
{
QMessageBox::warning(this, tr("Unable to Generate Codes"),
tr("The selected results did not produce valid AR code lines."));
return;
}
Common::IniFile game_ini_local;
const Common::IniFile game_ini_default =
SConfig::LoadDefaultGameIni(game_id, SConfig::GetInstance().GetRevision());
const std::string ini_path =
std::string(File::GetUserPath(D_GAMESETTINGS_IDX)).append(game_id).append(".ini");
game_ini_local.Load(ini_path);
std::vector<ActionReplay::ARCode> codes = ActionReplay::LoadCodes(game_ini_default, game_ini_local);
for (ActionReplay::ARCode& code : generated_codes)
{
std::erase_if(codes, [&](const ActionReplay::ARCode& existing_code) {
return existing_code.user_defined && existing_code.name == code.name;
});
codes.push_back(std::move(code));
}
ActionReplay::SaveCodes(&game_ini_local, codes);
if (!game_ini_local.Save(ini_path))
{
QMessageBox::warning(this, tr("Unable to Save Codes"),
tr("Failed to save generated codes to the local game INI."));
return;
}
ActionReplay::ApplyCodes(codes, game_id, SConfig::GetInstance().GetRevision());
QGuiApplication::clipboard()->setText(clipboard_text);
QMessageBox::information(this, tr("AR Codes Generated"),
tr("Saved %1 code(s) and copied the generated text to the clipboard.")
.arg(generated_codes.size()));
}
void CullingCodeFinderWidget::OnBrowseOutputFolder()
{
const QString current_path = m_output_folder_edit->text().trimmed();
const QString selected_path =
QFileDialog::getExistingDirectory(this, tr("Select Output Folder"), current_path);
if (!selected_path.isEmpty())
m_output_folder_edit->setText(selected_path);
}
void CullingCodeFinderWidget::OnOpenOutputFolder()
{
const QString output_path = m_output_folder_edit->text().trimmed();
if (output_path.isEmpty())
return;
File::CreateFullPath(output_path.toStdString() + DIR_SEP);
QDesktopServices::openUrl(QUrl::fromLocalFile(output_path));
}
void CullingCodeFinderWidget::OnResetResults()
{
auto& finder = CullingCodeFinder::GetInstance();
std::string error;
finder.ClearResults(&error);
if (!error.empty())
{
QMessageBox::warning(this, tr("Unable to Reset Results"), QString::fromStdString(error));
return;
}
m_last_result_count = -1;
UpdateDisplay();
}
void CullingCodeFinderWidget::UpdateResultsTable()
{
const auto results = CullingCodeFinder::GetInstance().GetResults();
const bool show_timeouts =
m_show_timeout_results_check && m_show_timeout_results_check->isChecked();
m_visible_result_indices.clear();
m_visible_result_indices.reserve(results.size());
for (int i = 0; i < static_cast<int>(results.size()); ++i)
{
if (!show_timeouts && results[static_cast<size_t>(i)].caused_timeout)
continue;
m_visible_result_indices.push_back(i);
}
QSignalBlocker blocker(m_results_table);
m_results_table->clearSelection();
m_results_table->setRowCount(static_cast<int>(m_visible_result_indices.size()));
for (int row = 0; row < static_cast<int>(m_visible_result_indices.size()); ++row)
{
const auto& result = results[static_cast<size_t>(m_visible_result_indices[row])];
auto* symbol_item = new QTableWidgetItem(QString::fromStdString(result.symbol_name));
auto* address_item = new QTableWidgetItem(QString::asprintf("%08X", result.address));
auto* template_item = new QTableWidgetItem(
QString::fromStdString(CullingCodeFinder::GetPatchTemplateName(result.template_kind)));
auto* draw_call_item = new QTableWidgetItem(QString::number(result.draw_call_delta));
auto* triangle_item = new QTableWidgetItem(QString::number(result.triangle_delta));
auto* object_item = new QTableWidgetItem(QString::number(result.object_delta));
auto* timeout_item = new QTableWidgetItem(result.caused_timeout ? tr("Yes") : tr("No"));
auto* screenshot_item = new QTableWidgetItem(PathLeaf(result.screenshot_path));
address_item->setTextAlignment(Qt::AlignCenter);
draw_call_item->setTextAlignment(Qt::AlignCenter);
triangle_item->setTextAlignment(Qt::AlignCenter);
object_item->setTextAlignment(Qt::AlignCenter);
timeout_item->setTextAlignment(Qt::AlignCenter);
m_results_table->setItem(row, 0, symbol_item);
m_results_table->setItem(row, 1, address_item);
m_results_table->setItem(row, 2, template_item);
m_results_table->setItem(row, 3, draw_call_item);
m_results_table->setItem(row, 4, triangle_item);
m_results_table->setItem(row, 5, object_item);
m_results_table->setItem(row, 6, timeout_item);
m_results_table->setItem(row, 7, screenshot_item);
}
m_results_table->resizeColumnsToContents();
}
void CullingCodeFinderWidget::UpdateButtonStates()
{
const auto state = CullingCodeFinder::GetInstance().GetState();
const bool actively_scanning = state == CullingCodeFinder::ScanState::Scanning;
const bool paused = state == CullingCodeFinder::ScanState::Paused;
const bool busy = actively_scanning || paused;
m_set_baseline_btn->setEnabled(!busy);
m_generate_map_btn->setEnabled(!busy);
m_savestate_slot_spin->setEnabled(!busy);
m_settle_frames_spin->setEnabled(!busy);
m_min_draw_call_spin->setEnabled(!busy);
m_min_triangle_spin->setEnabled(!busy);
m_min_object_spin->setEnabled(!busy);
m_candidate_limit_spin->setEnabled(!busy);
m_start_address_edit->setEnabled(!busy);
m_end_address_edit->setEnabled(!busy);
m_symbol_filter_edit->setEnabled(!busy);
m_turbo_during_scan_check->setEnabled(!busy);
m_mute_audio_during_scan_check->setEnabled(!busy);
m_auto_capture_reference_state_check->setEnabled(!busy);
m_output_folder_edit->setEnabled(!busy);
m_browse_output_btn->setEnabled(!busy);
for (QCheckBox* checkbox : m_template_checks)
{
if (checkbox)
checkbox->setEnabled(!busy);
}
m_start_btn->setEnabled(!busy);
m_restore_btn->setEnabled(!busy && !m_output_folder_edit->text().trimmed().isEmpty());
m_pause_btn->setEnabled(actively_scanning || paused);
m_pause_btn->setText(paused ? tr("Resume") : tr("Pause"));
m_stop_btn->setEnabled(busy);
m_show_timeout_results_check->setEnabled(true);
m_generate_ar_btn->setEnabled(m_results_table->rowCount() > 0);
m_reset_results_btn->setEnabled(!busy && !CullingCodeFinder::GetInstance().GetResults().empty());
m_open_output_btn->setEnabled(!m_output_folder_edit->text().trimmed().isEmpty());
}
void CullingCodeFinderWidget::ApplyConfigToWidgets()
{
const auto config = CullingCodeFinder::GetInstance().GetConfig();
m_savestate_slot_spin->setValue(config.savestate_slot);
m_settle_frames_spin->setValue(config.settle_frames);
m_min_draw_call_spin->setValue(config.min_draw_call_increase);
m_min_triangle_spin->setValue(config.min_triangle_increase);
m_min_object_spin->setValue(config.min_object_increase);
m_candidate_limit_spin->setValue(config.candidate_limit);
m_start_address_edit->setText(config.start_address == 0 ?
QString{} :
QString::asprintf("%08X", config.start_address));
m_end_address_edit->setText(config.end_address == 0 ?
QString{} :
QString::asprintf("%08X", config.end_address));
m_symbol_filter_edit->setText(QString::fromStdString(config.symbol_filter));
m_turbo_during_scan_check->setChecked(config.turbo_during_scan);
m_mute_audio_during_scan_check->setChecked(config.mute_audio_during_scan);
m_auto_capture_reference_state_check->setChecked(config.auto_capture_reference_state);
m_output_folder_edit->setText(QString::fromStdString(config.output_folder));
for (size_t i = 0; i < m_template_checks.size(); ++i)
{
if (m_template_checks[i])
m_template_checks[i]->setChecked(config.enabled_templates[i]);
}
}
@@ -0,0 +1,90 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <array>
#include <vector>
class QCheckBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QProgressBar;
class QSpinBox;
class QTableWidget;
class QTimer;
class CullingCodeFinderWidget : public QDialog
{
Q_OBJECT
public:
explicit CullingCodeFinderWidget(QWidget* parent = nullptr);
private:
void closeEvent(QCloseEvent* event) override;
void CreateWidgets();
void ConnectSignals();
void RefreshStaticInfo();
void UpdateDisplay();
void OnSetBaseline();
void OnStartScan();
void OnRestoreSession();
void OnPauseScan();
void OnStopScan();
void OnGenerateMap();
void OnGenerateARCode();
void OnBrowseOutputFolder();
void OnOpenOutputFolder();
void OnResetResults();
void UpdateResultsTable();
void UpdateButtonStates();
void ApplyConfigToWidgets();
QLabel* m_live_draw_calls = nullptr;
QLabel* m_live_triangles = nullptr;
QLabel* m_live_prims = nullptr;
QLabel* m_live_objects = nullptr;
QLabel* m_baseline_label = nullptr;
QPushButton* m_set_baseline_btn = nullptr;
QLabel* m_map_path_label = nullptr;
QPushButton* m_generate_map_btn = nullptr;
QLineEdit* m_output_folder_edit = nullptr;
QPushButton* m_browse_output_btn = nullptr;
QPushButton* m_open_output_btn = nullptr;
QSpinBox* m_savestate_slot_spin = nullptr;
QSpinBox* m_settle_frames_spin = nullptr;
QLabel* m_settle_frames_help = nullptr;
QSpinBox* m_min_draw_call_spin = nullptr;
QSpinBox* m_min_triangle_spin = nullptr;
QSpinBox* m_min_object_spin = nullptr;
QSpinBox* m_candidate_limit_spin = nullptr;
QLineEdit* m_start_address_edit = nullptr;
QLineEdit* m_end_address_edit = nullptr;
QLineEdit* m_symbol_filter_edit = nullptr;
QCheckBox* m_turbo_during_scan_check = nullptr;
QCheckBox* m_mute_audio_during_scan_check = nullptr;
QCheckBox* m_auto_capture_reference_state_check = nullptr;
std::array<QCheckBox*, 4> m_template_checks{};
QPushButton* m_start_btn = nullptr;
QPushButton* m_restore_btn = nullptr;
QPushButton* m_pause_btn = nullptr;
QPushButton* m_stop_btn = nullptr;
QProgressBar* m_progress_bar = nullptr;
QLabel* m_status_label = nullptr;
QTableWidget* m_results_table = nullptr;
QCheckBox* m_show_timeout_results_check = nullptr;
QPushButton* m_generate_ar_btn = nullptr;
QPushButton* m_reset_results_btn = nullptr;
std::vector<int> m_visible_result_indices;
int m_last_result_count = -1;
QTimer* m_update_timer = nullptr;
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,68 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <vector>
#include <QCloseEvent>
#include <QDialog>
class QCheckBox;
class QComboBox;
class QDoubleSpinBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTimer;
class QVBoxLayout;
class QWidget;
class ShaderHunterWidget : public QDialog
{
Q_OBJECT
public:
explicit ShaderHunterWidget(QWidget* parent = nullptr);
signals:
void OverridesChanged();
private:
void closeEvent(QCloseEvent* event) override;
void CreateWidgets();
void ConnectSignals();
void UpdateDisplay();
void ShowDrawCallDialog();
void SetSelectedDrawCallRange(int start, int end, int total);
void SetSelectedTextureHashes(const std::vector<uint64_t>& hashes);
void SaveCurrentShader();
void DumpCurrentShader();
void ShowTexturesDialog();
QVBoxLayout* m_main_layout = nullptr;
QCheckBox* m_enable_checkbox;
QComboBox* m_hunting_option_combo;
QCheckBox* m_debug_log_checkbox;
QComboBox* m_type_combo;
QLabel* m_hash_label;
QLabel* m_position_label;
QPushButton* m_prev_button;
QPushButton* m_next_button;
QLineEdit* m_shader_name_edit;
QComboBox* m_handling_combo;
QLabel* m_units_per_meter_label;
QDoubleSpinBox* m_units_per_meter_spin;
QPushButton* m_save_button;
QPushButton* m_dump_button;
QPushButton* m_textures_button;
QPushButton* m_draw_calls_button;
QLabel* m_selected_draw_call_label;
QComboBox* m_texture_filter_mode_combo;
QLabel* m_selected_texture_label;
QTimer* m_update_timer;
int m_saved_element_start = -1;
int m_saved_element_end = -1;
int m_saved_element_total = 0;
std::vector<uint64_t> m_saved_texture_filters;
};
+7 -1
View File
@@ -36,13 +36,19 @@
#include "UICommon/DiscordPresence.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/CullingCodeFinder.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/Present.h"
#include "VideoCommon/VideoConfig.h"
Host::Host()
{
State::SetOnAfterLoadCallback([] { Host_UpdateDisasmDialog(); });
State::SetOnAfterLoadCallback([] {
auto& finder = CullingCodeFinder::GetInstance();
finder.NotifySavestateLoaded();
finder.AdvanceScan(Core::System::GetInstance());
Host_UpdateDisasmDialog();
});
}
Host::~Host()
+326 -4
View File
@@ -22,6 +22,7 @@
#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/UISettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/FreeLookManager.h"
#include "Core/HotkeyManager.h"
@@ -37,12 +38,19 @@
#include "DolphinQt/Settings.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "VideoCommon/CullingCodeFinder.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/ShaderHunter.h"
#include "VideoCommon/VideoConfig.h"
#ifdef ENABLE_VR
#include "VideoCommon/VR/OpenXRManager.h"
#endif
constexpr const char* DUBOIS_ALGORITHM_SHADER = "dubois";
constexpr float HOTKEY_VR_UNITS_PER_METER_MIN = 0.01f;
constexpr float HOTKEY_VR_UNITS_PER_METER_MAX = 1000.0f;
HotkeyScheduler::HotkeyScheduler() : m_stop_requested(false)
{
@@ -483,15 +491,329 @@ void HotkeyScheduler::Run()
if (IsHotkey(HK_TOGGLE_TEXTURES))
Config::SetCurrent(Config::GFX_HIRES_TEXTURES, !Config::Get(Config::GFX_HIRES_TEXTURES));
Core::SetIsThrottlerTempDisabled(IsHotkey(HK_TOGGLE_THROTTLE, true));
// VR
auto ToggleVRSetting = [](const auto& setting, const char* label) {
const bool new_value = !Config::Get(setting);
Config::SetCurrent(setting, new_value);
OSD::AddMessage(fmt::format("{}: {}", label, new_value ? "Enabled" : "Disabled"));
};
if (IsHotkey(HK_TOGGLE_THROTTLE, true) && !Config::Get(Config::MAIN_AUDIO_MUTED) &&
Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT))
auto AdjustVRFloatSetting = [](const auto& setting, const float delta, const float min_value,
const float max_value, const char* label, const int precision) {
const float current = Config::Get(setting);
const float new_value = std::clamp(current + delta, min_value, max_value);
Config::SetCurrent(setting, new_value);
OSD::AddMessage(fmt::format("{}: {:.{}f}", label, new_value, precision));
};
if (IsHotkey(HK_VR_TOGGLE_OPENXR))
ToggleVRSetting(Config::GFX_VR_ENABLE_OPENXR, "OpenXR");
if (IsHotkey(HK_VR_RESET_POSITION))
{
#ifdef ENABLE_VR
if (VR::g_openxr)
{
VR::g_openxr->RequestRecenter();
OSD::AddMessage("VR Position: Recenter requested");
}
else
{
OSD::AddMessage("VR Position: OpenXR is not active");
}
#else
OSD::AddMessage("VR Position: VR backend not enabled");
#endif
}
if (IsHotkey(HK_VR_DECREASE_UNITS_PER_METER))
{
AdjustVRFloatSetting(Config::GFX_VR_UNITS_PER_METER, -Config::GFX_VR_UNITS_PER_METER_STEP,
HOTKEY_VR_UNITS_PER_METER_MIN, HOTKEY_VR_UNITS_PER_METER_MAX,
"Units Per Meter", 2);
}
if (IsHotkey(HK_VR_INCREASE_UNITS_PER_METER))
{
AdjustVRFloatSetting(Config::GFX_VR_UNITS_PER_METER, Config::GFX_VR_UNITS_PER_METER_STEP,
HOTKEY_VR_UNITS_PER_METER_MIN, HOTKEY_VR_UNITS_PER_METER_MAX,
"Units Per Meter", 2);
}
if (IsHotkey(HK_VR_DECREASE_LEAN_BACK_ANGLE))
{
AdjustVRFloatSetting(Config::GFX_VR_LEAN_BACK_ANGLE, -Config::GFX_VR_LEAN_BACK_ANGLE_STEP,
Config::GFX_VR_LEAN_BACK_ANGLE_MIN,
Config::GFX_VR_LEAN_BACK_ANGLE_MAX, "Lean Back Angle", 1);
}
if (IsHotkey(HK_VR_INCREASE_LEAN_BACK_ANGLE))
{
AdjustVRFloatSetting(Config::GFX_VR_LEAN_BACK_ANGLE, Config::GFX_VR_LEAN_BACK_ANGLE_STEP,
Config::GFX_VR_LEAN_BACK_ANGLE_MIN,
Config::GFX_VR_LEAN_BACK_ANGLE_MAX, "Lean Back Angle", 1);
}
if (IsHotkey(HK_VR_DECREASE_CAMERA_FORWARD))
{
AdjustVRFloatSetting(Config::GFX_VR_CAMERA_FORWARD, -Config::GFX_VR_CAMERA_FORWARD_STEP,
Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX,
"Camera Forward", 2);
}
if (IsHotkey(HK_VR_INCREASE_CAMERA_FORWARD))
{
AdjustVRFloatSetting(Config::GFX_VR_CAMERA_FORWARD, Config::GFX_VR_CAMERA_FORWARD_STEP,
Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX,
"Camera Forward", 2);
}
if (IsHotkey(HK_VR_TOGGLE_VIRTUAL_SCREEN))
ToggleVRSetting(Config::GFX_VR_VIRTUAL_SCREEN, "Virtual Screen");
if (IsHotkey(HK_VR_DECREASE_SCREEN_DISTANCE))
{
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_DISTANCE, -Config::GFX_VR_SCREEN_DISTANCE_STEP,
Config::GFX_VR_SCREEN_DISTANCE_MIN,
Config::GFX_VR_SCREEN_DISTANCE_MAX, "Screen Distance", 2);
}
if (IsHotkey(HK_VR_INCREASE_SCREEN_DISTANCE))
{
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_DISTANCE, Config::GFX_VR_SCREEN_DISTANCE_STEP,
Config::GFX_VR_SCREEN_DISTANCE_MIN,
Config::GFX_VR_SCREEN_DISTANCE_MAX, "Screen Distance", 2);
}
if (IsHotkey(HK_VR_DECREASE_SCREEN_SIZE))
{
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_SIZE, -Config::GFX_VR_SCREEN_SIZE_STEP,
Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX,
"Screen Size", 2);
}
if (IsHotkey(HK_VR_INCREASE_SCREEN_SIZE))
{
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_SIZE, Config::GFX_VR_SCREEN_SIZE_STEP,
Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX,
"Screen Size", 2);
}
if (IsHotkey(HK_VR_DECREASE_SCREEN_CURVATURE))
{
AdjustVRFloatSetting(Config::GFX_VR_HEAD_LOCKED_CURVATURE,
-Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX, "Screen Curvature", 2);
}
if (IsHotkey(HK_VR_INCREASE_SCREEN_CURVATURE))
{
AdjustVRFloatSetting(Config::GFX_VR_HEAD_LOCKED_CURVATURE,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX, "Screen Curvature", 2);
}
if (IsHotkey(HK_VR_TOGGLE_DONT_CLEAR_SCREEN))
ToggleVRSetting(Config::GFX_VR_DONT_CLEAR_SCREEN, "Don't Clear Screen");
if (IsHotkey(HK_VR_TOGGLE_FORCE_VBI))
ToggleVRSetting(Config::GFX_VR_AUTO_VBI_FROM_HMD, "Force VBI");
if (IsHotkey(HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS))
ToggleVRSetting(Config::GFX_VR_REMOVE_BARS, "Remove Cinematic Bars");
// VR Shader Hunting
auto& shader_hunter = ShaderHunter::GetInstance();
static ShaderHunter::HandlingType shader_hotkey_handling = ShaderHunter::HandlingType::Skip;
auto ShaderTypeLabel = [](ShaderHunter::ShaderType type) {
switch (type)
{
case ShaderHunter::ShaderType::Pixel:
return "Pixel";
case ShaderHunter::ShaderType::Vertex:
return "Vertex";
case ShaderHunter::ShaderType::Geometry:
return "Geometry";
default:
return "Unknown";
}
};
auto HandlingTypeLabel = [](ShaderHunter::HandlingType type) {
switch (type)
{
case ShaderHunter::HandlingType::Skip:
return "Skip";
case ShaderHunter::HandlingType::Screen:
return "Screen";
case ShaderHunter::HandlingType::HeadLocked:
return "Head Locked";
case ShaderHunter::HandlingType::Fullscreen:
return "Fullscreen";
case ShaderHunter::HandlingType::FullscreenMono:
return "Fullscreen";
case ShaderHunter::HandlingType::Flag:
return "Flag";
case ShaderHunter::HandlingType::UnitsPerMeter:
return "Units Per Meter";
default:
return "Unknown";
}
};
auto ShowSelectedShader = [&shader_hunter, ShaderTypeLabel]() {
const int pos = shader_hunter.GetSelectedPosition();
const int total = shader_hunter.GetTotalCount();
const u64 hash = shader_hunter.GetSelectedHash();
if (pos < 0 || total <= 0 || hash == ~0ULL)
{
OSD::AddMessage(fmt::format("Shader: {} (none)",
ShaderTypeLabel(shader_hunter.GetActiveType())));
return;
}
OSD::AddMessage(fmt::format("Shader: {} {:08x} ({}/{})",
ShaderTypeLabel(shader_hunter.GetActiveType()),
static_cast<u32>(hash), pos + 1, total));
};
if (IsHotkey(HK_VR_SHADER_TOGGLE_HUNTING))
{
const bool enabled = !shader_hunter.IsEnabled();
shader_hunter.SetEnabled(enabled);
OSD::AddMessage(fmt::format("Shader Hunting: {}", enabled ? "Enabled" : "Disabled"));
}
if (IsHotkey(HK_VR_SHADER_CYCLE_HUNTING_OPTION))
{
const auto new_option = shader_hunter.GetHuntingOption() == ShaderHunter::HuntingOption::Skip ?
ShaderHunter::HuntingOption::Pink :
ShaderHunter::HuntingOption::Skip;
shader_hunter.SetHuntingOption(new_option);
OSD::AddMessage(fmt::format("Hunting Option: {}",
new_option == ShaderHunter::HuntingOption::Pink ? "Pink" :
"Skip"));
}
if (IsHotkey(HK_VR_SHADER_CYCLE_TYPE))
{
const int next_type =
(static_cast<int>(shader_hunter.GetActiveType()) + 1) %
static_cast<int>(ShaderHunter::ShaderType::Count);
shader_hunter.SetActiveType(static_cast<ShaderHunter::ShaderType>(next_type));
OSD::AddMessage(
fmt::format("Shader Type: {}", ShaderTypeLabel(shader_hunter.GetActiveType())));
}
if (IsHotkey(HK_VR_SHADER_CYCLE_HANDLING))
{
switch (shader_hotkey_handling)
{
case ShaderHunter::HandlingType::Skip:
shader_hotkey_handling = ShaderHunter::HandlingType::Screen;
break;
case ShaderHunter::HandlingType::Screen:
shader_hotkey_handling = ShaderHunter::HandlingType::HeadLocked;
break;
case ShaderHunter::HandlingType::HeadLocked:
shader_hotkey_handling = ShaderHunter::HandlingType::Fullscreen;
break;
case ShaderHunter::HandlingType::Fullscreen:
shader_hotkey_handling = ShaderHunter::HandlingType::Flag;
break;
case ShaderHunter::HandlingType::FullscreenMono:
shader_hotkey_handling = ShaderHunter::HandlingType::Flag;
break;
case ShaderHunter::HandlingType::Flag:
shader_hotkey_handling = ShaderHunter::HandlingType::UnitsPerMeter;
break;
case ShaderHunter::HandlingType::UnitsPerMeter:
default:
shader_hotkey_handling = ShaderHunter::HandlingType::Skip;
break;
}
OSD::AddMessage(
fmt::format("Override Handling: {}", HandlingTypeLabel(shader_hotkey_handling)));
}
if (IsHotkey(HK_VR_SHADER_PREV))
{
shader_hunter.PrevShader();
ShowSelectedShader();
}
if (IsHotkey(HK_VR_SHADER_NEXT))
{
shader_hunter.NextShader();
ShowSelectedShader();
}
if (IsHotkey(HK_VR_SHADER_PREV_TEXTURE_HASH))
{
shader_hunter.PrevTextureHash();
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
if (texture_hash == 0)
OSD::AddMessage("Texture Hash: (none)");
else
OSD::AddMessage(
fmt::format("Texture Hash: {:016x}", static_cast<unsigned long long>(texture_hash)));
}
if (IsHotkey(HK_VR_SHADER_NEXT_TEXTURE_HASH))
{
shader_hunter.NextTextureHash();
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
if (texture_hash == 0)
OSD::AddMessage("Texture Hash: (none)");
else
OSD::AddMessage(
fmt::format("Texture Hash: {:016x}", static_cast<unsigned long long>(texture_hash)));
}
if (IsHotkey(HK_VR_SHADER_TOGGLE_TEXTURE_HASH))
{
const bool enabled = shader_hunter.ToggleSelectedTextureHashFilter();
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
if (texture_hash == 0)
{
OSD::AddMessage("Texture Hash: (none)");
}
else
{
OSD::AddMessage(
fmt::format("Texture Hash {}: {:016x}", enabled ? "added" : "removed",
static_cast<unsigned long long>(texture_hash)));
}
}
if (IsHotkey(HK_VR_SHADER_SAVE_OVERRIDE))
{
const std::string game_id = SConfig::GetInstance().GetGameID();
if (game_id.empty())
{
OSD::AddMessage("Save Shader Override: no game is running");
}
else if (shader_hunter.SaveSelectedShaderOverride(game_id, shader_hotkey_handling))
{
Settings::Instance().NotifyShaderOverridesChanged();
OSD::AddMessage(fmt::format("Saved {} shader override to {}.ini",
HandlingTypeLabel(shader_hotkey_handling), game_id));
}
else
{
OSD::AddMessage("Save Shader Override: no shader selected");
}
}
const bool throttle_hotkey_active = IsHotkey(HK_TOGGLE_THROTTLE, true);
const bool finder_forces_turbo =
CullingCodeFinder::GetInstance().IsTurboRuntimeOverrideActive();
const bool finder_forces_audio_mute =
CullingCodeFinder::GetInstance().IsAudioMuteRuntimeOverrideActive();
const bool wants_temp_audio_mute =
finder_forces_audio_mute ||
(throttle_hotkey_active && Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT));
Core::SetIsThrottlerTempDisabled(throttle_hotkey_active || finder_forces_turbo);
if (wants_temp_audio_mute && !Config::Get(Config::MAIN_AUDIO_MUTED))
{
Config::SetCurrent(Config::MAIN_AUDIO_MUTED, true);
AudioCommon::UpdateSoundStream(system);
}
else if (!IsHotkey(HK_TOGGLE_THROTTLE, true) && Config::Get(Config::MAIN_AUDIO_MUTED) &&
else if (!wants_temp_audio_mute && Config::Get(Config::MAIN_AUDIO_MUTED) &&
Config::GetActiveLayerForConfig(Config::MAIN_AUDIO_MUTED) ==
Config::LayerType::CurrentRun)
{
+27 -3
View File
@@ -211,11 +211,27 @@ static std::vector<std::string> StringListToStdVector(QStringList list)
return result;
}
static QString GetMainWindowTitle()
{
QString title = QString::fromStdString(Common::GetScmRevStr());
if (title.startsWith(QStringLiteral("Dolphin ")))
title.replace(0, QStringLiteral("Dolphin").size(), QStringLiteral("Dolphin-OpenXR"));
if (title.endsWith(QStringLiteral("-dirty")))
{
title.chop(QStringLiteral("-dirty").size());
title.append(QStringLiteral(" -dirty"));
}
return title;
}
MainWindow::MainWindow(Core::System& system, std::unique_ptr<BootParameters> boot_parameters,
const std::string& movie_path)
: QMainWindow(nullptr), m_system(system)
{
setWindowTitle(QString::fromStdString(Common::GetScmRevStr()));
setWindowTitle(GetMainWindowTitle());
setWindowIcon(Resources::GetAppIcon());
setUnifiedTitleAndToolBarOnMac(true);
setAcceptDrops(true);
@@ -694,6 +710,7 @@ void MainWindow::ConnectToolBar()
connect(m_tool_bar, &ToolBar::SettingsPressed, this, &MainWindow::ShowSettingsWindow);
connect(m_tool_bar, &ToolBar::ControllersPressed, this, &MainWindow::ShowControllersWindow);
connect(m_tool_bar, &ToolBar::GraphicsPressed, this, &MainWindow::ShowGraphicsWindow);
connect(m_tool_bar, &ToolBar::VRPressed, this, &MainWindow::ShowVRWindow);
connect(m_tool_bar, &ToolBar::StepPressed, m_code_widget, &CodeWidget::Step);
connect(m_tool_bar, &ToolBar::StepOverPressed, m_code_widget, &CodeWidget::StepOver);
@@ -1277,7 +1294,7 @@ void MainWindow::HideRenderWidget(bool reinit, bool is_exit)
m_rendering_to_main = false;
m_stack->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
disconnect(Host::GetInstance(), &Host::RequestTitle, this, &MainWindow::setWindowTitle);
setWindowTitle(QString::fromStdString(Common::GetScmRevStr()));
setWindowTitle(GetMainWindowTitle());
}
// The following code works around a driver bug that would lead to Dolphin crashing when changing
@@ -1404,6 +1421,12 @@ void MainWindow::ShowGraphicsWindow()
m_settings_window->SelectPane(SettingsWindowPaneIndex::Graphics);
}
void MainWindow::ShowVRWindow()
{
ShowSettingsWindow();
m_settings_window->SelectPane(SettingsWindowPaneIndex::VR);
}
void MainWindow::ShowNetPlaySetupDialog()
{
m_netplay_setup_dialog->show();
@@ -2024,7 +2047,8 @@ void MainWindow::ShowTASInput()
for (int i = 0; i < num_wii_controllers; i++)
{
if (Config::Get(Config::GetInfoForWiimoteSource(i)) == WiimoteSource::Emulated &&
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(i));
if ((source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR) &&
(!Core::IsRunning(m_system) || m_system.IsWii()))
{
m_wii_tas_input_windows[i]->show();
+1
View File
@@ -171,6 +171,7 @@ private:
void ShowAudioWindow();
void ShowControllersWindow();
void ShowGraphicsWindow();
void ShowVRWindow();
void ShowFreeLookWindow();
void ShowAboutDialog();
void ShowHotkeyDialog();
+5
View File
@@ -479,6 +479,11 @@ void Settings::RefreshGameList()
emit GameListRefreshRequested();
}
void Settings::NotifyShaderOverridesChanged()
{
emit ShaderOverridesChanged();
}
void Settings::NotifyRefreshGameListStarted()
{
emit GameListRefreshStarted();
+2
View File
@@ -104,6 +104,7 @@ public:
QString GetDefaultGame() const;
void SetDefaultGame(QString path);
void RefreshGameList();
void NotifyShaderOverridesChanged();
void NotifyRefreshGameListStarted();
void NotifyRefreshGameListComplete();
void NotifyMetadataRefreshComplete();
@@ -200,6 +201,7 @@ signals:
void TitleDBReloadRequested();
void MetadataRefreshRequested();
void MetadataRefreshCompleted();
void ShaderOverridesChanged();
void AutoRefreshToggled(bool enabled);
void CursorVisibilityChanged();
void LockCursorChanged();
@@ -101,9 +101,15 @@ void OnScreenDisplayPane::CreateLayout()
m_show_statistics = new ConfigBool(tr("Show Statistics"), Config::GFX_OVERLAY_STATS);
m_show_proj_statistics =
new ConfigBool(tr("Show Projection Statistics"), Config::GFX_OVERLAY_PROJ_STATS);
m_show_shader_flags =
new ConfigBool(tr("Show Shader Flags"), Config::GFX_OVERLAY_SHADER_FLAGS);
m_show_shader_hunting =
new ConfigBool(tr("Shader Hunting"), Config::GFX_OVERLAY_SHADER_HUNTING);
debug_layout->addWidget(m_show_statistics, 0, 0);
debug_layout->addWidget(m_show_proj_statistics, 0, 1);
debug_layout->addWidget(m_show_shader_flags, 1, 0);
debug_layout->addWidget(m_show_shader_hunting, 1, 1);
// Stack GroupBoxes
auto* main_layout = new QVBoxLayout;
@@ -217,6 +223,14 @@ void OnScreenDisplayPane::AddDescriptions()
static const char TR_SHOW_PROJ_STATS_DESCRIPTION[] =
QT_TR_NOOP("Shows various projection statistics.<br><br><dolphin_emphasis>If unsure, "
"leave this unchecked.</dolphin_emphasis>");
static const char TR_SHOW_SHADER_FLAGS_DESCRIPTION[] =
QT_TR_NOOP("Shows ShaderHunter condition flag names and their current active/inactive "
"status in the top-left corner."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static const char TR_SHOW_SHADER_HUNTING_DESCRIPTION[] =
QT_TR_NOOP("Shows Shader Hunting status in the top-left corner, including the selected "
"shader hash, draw-call filter state, and selected texture hashes."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
m_enable_osd->SetDescription(tr(TR_ENABLE_OSD_DESCRIPTION));
m_font_size->SetDescription(tr(TR_OSD_FONT_SIZE_DESCRIPTION));
@@ -242,4 +256,6 @@ void OnScreenDisplayPane::AddDescriptions()
m_show_statistics->SetDescription(tr(TR_SHOW_STATS_DESCRIPTION));
m_show_proj_statistics->SetDescription(tr(TR_SHOW_PROJ_STATS_DESCRIPTION));
m_show_shader_flags->SetDescription(tr(TR_SHOW_SHADER_FLAGS_DESCRIPTION));
m_show_shader_hunting->SetDescription(tr(TR_SHOW_SHADER_HUNTING_DESCRIPTION));
}
@@ -48,4 +48,6 @@ private:
// Debug
ConfigBool* m_show_statistics;
ConfigBool* m_show_proj_statistics;
ConfigBool* m_show_shader_flags;
ConfigBool* m_show_shader_hunting;
};
+575
View File
@@ -0,0 +1,575 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Settings/VRPane.h"
#include <QGroupBox>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QTabWidget>
#include <QVBoxLayout>
#include "Core/Config/GraphicsSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "DolphinQt/Config/HideObjectAddEditDialog.h"
#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/ConfigControls/ConfigChoice.h"
#include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h"
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
#include "DolphinQt/Debugger/CullingCodeFinderWidget.h"
#include "DolphinQt/Debugger/ShaderHunterWidget.h"
#include "DolphinQt/Settings.h"
#include "VideoCommon/HideObjectEngine.h"
#include "VideoCommon/VideoConfig.h"
VRPane::VRPane(QWidget* parent) : QWidget(parent)
{
auto* main_layout = new QVBoxLayout;
auto* tabs = new QTabWidget(this);
auto* general_tab = new QWidget(this);
auto* general_layout = new QVBoxLayout;
auto* hack_tab = new QWidget(this);
auto* hack_layout = new QVBoxLayout;
auto* tools_tab = new QWidget(this);
auto* tools_tab_layout = new QVBoxLayout;
general_tab->setLayout(general_layout);
hack_tab->setLayout(hack_layout);
tools_tab->setLayout(tools_tab_layout);
tabs->addTab(general_tab, tr("General"));
tabs->addTab(hack_tab, tr("Hacks"));
tabs->addTab(tools_tab, tr("Tools"));
auto* openxr_group = new QGroupBox(tr("OpenXR"));
auto* openxr_layout = new QGridLayout;
openxr_group->setLayout(openxr_layout);
m_enable_openxr = new ConfigBool(tr("Enable OpenXR"), Config::GFX_VR_ENABLE_OPENXR);
m_auto_immediate_xfb =
new ConfigBool(tr("Force Immediately Present XFB"),
Config::GFX_VR_AUTO_IMMEDIATE_XFB);
m_units_per_meter = new ConfigFloatSlider(Config::GFX_VR_UNITS_PER_METER_MIN,
Config::GFX_VR_UNITS_PER_METER_MAX,
Config::GFX_VR_UNITS_PER_METER,
Config::GFX_VR_UNITS_PER_METER_STEP);
m_units_per_meter_value = new QLabel();
m_lean_back_angle = new ConfigFloatSlider(Config::GFX_VR_LEAN_BACK_ANGLE_MIN,
Config::GFX_VR_LEAN_BACK_ANGLE_MAX,
Config::GFX_VR_LEAN_BACK_ANGLE,
Config::GFX_VR_LEAN_BACK_ANGLE_STEP);
m_lean_back_angle_value = new QLabel();
m_camera_forward = new ConfigFloatSlider(Config::GFX_VR_CAMERA_FORWARD_MIN,
Config::GFX_VR_CAMERA_FORWARD_MAX,
Config::GFX_VR_CAMERA_FORWARD,
Config::GFX_VR_CAMERA_FORWARD_STEP);
m_camera_forward_value = new QLabel();
openxr_layout->addWidget(m_enable_openxr, 0, 0, 1, 3);
m_ar_mode = new ConfigBool(tr("AR Mode (Passthrough)"), Config::GFX_VR_AR_MODE);
openxr_layout->addWidget(m_ar_mode, 1, 0, 1, 3);
m_ar_background_alpha =
new ConfigFloatSlider(Config::GFX_VR_AR_BACKGROUND_ALPHA_MIN,
Config::GFX_VR_AR_BACKGROUND_ALPHA_MAX,
Config::GFX_VR_AR_BACKGROUND_ALPHA,
Config::GFX_VR_AR_BACKGROUND_ALPHA_STEP);
m_ar_background_alpha_value = new QLabel();
openxr_layout->addWidget(
new ConfigFloatLabel(tr("AR Background Alpha:"), m_ar_background_alpha), 16, 0);
openxr_layout->addWidget(m_ar_background_alpha, 16, 1);
openxr_layout->addWidget(m_ar_background_alpha_value, 16, 2);
m_ar_background_alpha_value->setText(
QString::asprintf("%.2f", m_ar_background_alpha->GetValue()));
connect(m_ar_background_alpha, &ConfigFloatSlider::valueChanged, this, [this] {
m_ar_background_alpha_value->setText(
QString::asprintf("%.2f", m_ar_background_alpha->GetValue()));
});
openxr_layout->addWidget(new ConfigFloatLabel(tr("Units per Meter:"), m_units_per_meter), 2, 0);
openxr_layout->addWidget(m_units_per_meter, 2, 1);
openxr_layout->addWidget(m_units_per_meter_value, 2, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Lean Back Angle (deg):"), m_lean_back_angle), 3,
0);
openxr_layout->addWidget(m_lean_back_angle, 3, 1);
openxr_layout->addWidget(m_lean_back_angle_value, 3, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 4, 0);
openxr_layout->addWidget(m_camera_forward, 4, 1);
openxr_layout->addWidget(m_camera_forward_value, 4, 2);
m_units_per_meter_value->setText(QString::asprintf("%.1f", m_units_per_meter->GetValue()));
connect(m_units_per_meter, &ConfigFloatSlider::valueChanged, this, [this] {
m_units_per_meter_value->setText(QString::asprintf("%.1f", m_units_per_meter->GetValue()));
});
m_lean_back_angle_value->setText(QString::asprintf("%.1f", m_lean_back_angle->GetValue()));
connect(m_lean_back_angle, &ConfigFloatSlider::valueChanged, this, [this] {
m_lean_back_angle_value->setText(QString::asprintf("%.1f", m_lean_back_angle->GetValue()));
});
m_camera_forward_value->setText(QString::asprintf("%.1f", m_camera_forward->GetValue()));
connect(m_camera_forward, &ConfigFloatSlider::valueChanged, this, [this] {
m_camera_forward_value->setText(QString::asprintf("%.1f", m_camera_forward->GetValue()));
});
// Virtual screen settings
m_virtual_screen = new ConfigBool(tr("Virtual Screen for 2D Content"), Config::GFX_VR_VIRTUAL_SCREEN);
openxr_layout->addWidget(m_virtual_screen, 5, 0, 1, 3);
m_screen_distance = new ConfigFloatSlider(Config::GFX_VR_SCREEN_DISTANCE_MIN,
Config::GFX_VR_SCREEN_DISTANCE_MAX,
Config::GFX_VR_SCREEN_DISTANCE,
Config::GFX_VR_SCREEN_DISTANCE_STEP);
m_screen_distance_value = new QLabel();
m_screen_size = new ConfigFloatSlider(Config::GFX_VR_SCREEN_SIZE_MIN,
Config::GFX_VR_SCREEN_SIZE_MAX,
Config::GFX_VR_SCREEN_SIZE,
Config::GFX_VR_SCREEN_SIZE_STEP);
m_screen_size_value = new QLabel();
m_head_locked_curvature =
new ConfigFloatSlider(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX,
Config::GFX_VR_HEAD_LOCKED_CURVATURE,
Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP);
m_head_locked_curvature_value = new QLabel();
openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Distance (m):"), m_screen_distance), 6, 0);
openxr_layout->addWidget(m_screen_distance, 6, 1);
openxr_layout->addWidget(m_screen_distance_value, 6, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 7, 0);
openxr_layout->addWidget(m_screen_size, 7, 1);
openxr_layout->addWidget(m_screen_size_value, 7, 2);
openxr_layout->addWidget(
new ConfigFloatLabel(tr("Head Locked Curvature:"), m_head_locked_curvature), 8, 0);
openxr_layout->addWidget(m_head_locked_curvature, 8, 1);
openxr_layout->addWidget(m_head_locked_curvature_value, 8, 2);
m_screen_distance_value->setText(QString::asprintf("%.1f", m_screen_distance->GetValue()));
connect(m_screen_distance, &ConfigFloatSlider::valueChanged, this, [this] {
m_screen_distance_value->setText(QString::asprintf("%.1f", m_screen_distance->GetValue()));
});
m_screen_size_value->setText(QString::asprintf("%.1f", m_screen_size->GetValue()));
connect(m_screen_size, &ConfigFloatSlider::valueChanged, this, [this] {
m_screen_size_value->setText(QString::asprintf("%.1f", m_screen_size->GetValue()));
});
m_head_locked_curvature_value->setText(
QString::asprintf("%.2f", m_head_locked_curvature->GetValue()));
connect(m_head_locked_curvature, &ConfigFloatSlider::valueChanged, this, [this] {
m_head_locked_curvature_value->setText(
QString::asprintf("%.2f", m_head_locked_curvature->GetValue()));
});
m_opcode_replay_mode = new ConfigChoiceMap<OpenXROpcodeReplayMode>(
{{tr("Off"), OpenXROpcodeReplayMode::Off},
{tr("60 to 90"), OpenXROpcodeReplayMode::Replay60To90},
{tr("30 to 90"), OpenXROpcodeReplayMode::Replay30To90}},
Config::GFX_VR_OPCODE_REPLAY);
openxr_layout->addWidget(new QLabel(tr("Opcode Replay:")), 9, 0);
openxr_layout->addWidget(m_opcode_replay_mode, 9, 1, 1, 2);
m_auto_vbi_from_hmd = new ConfigBool(tr("Force VBI Frequency to 90 Hz in VR"),
Config::GFX_VR_AUTO_VBI_FROM_HMD);
openxr_layout->addWidget(m_auto_vbi_from_hmd, 10, 0, 1, 3);
m_clear_efb_slider = new ConfigSlider(Config::GFX_VR_CLEAR_EFB_MIN,
Config::GFX_VR_CLEAR_EFB_MAX,
Config::GFX_VR_CLEAR_EFB_COPIES,
Config::GFX_VR_CLEAR_EFB_STEP);
m_clear_efb_value = new QLabel();
openxr_layout->addWidget(new ConfigSliderLabel(tr("Clear EFB (min width):"), m_clear_efb_slider),
11, 0);
openxr_layout->addWidget(m_clear_efb_slider, 11, 1);
openxr_layout->addWidget(m_clear_efb_value, 11, 2);
auto update_clear_efb_label = [this] {
const int val = m_clear_efb_slider->value();
m_clear_efb_value->setText(val == 0 ? tr("Off") : QString::number(val));
};
update_clear_efb_label();
connect(m_clear_efb_slider, &ConfigSlider::valueChanged, this, update_clear_efb_label);
// VR Gamma
m_vr_gamma = new ConfigFloatSlider(Config::GFX_VR_GAMMA_MIN, Config::GFX_VR_GAMMA_MAX,
Config::GFX_VR_GAMMA, Config::GFX_VR_GAMMA_STEP);
m_vr_gamma_value = new QLabel();
openxr_layout->addWidget(new ConfigFloatLabel(tr("VR Gamma:"), m_vr_gamma), 12, 0);
openxr_layout->addWidget(m_vr_gamma, 12, 1);
openxr_layout->addWidget(m_vr_gamma_value, 12, 2);
auto update_gamma_label = [this] {
const float val = m_vr_gamma->GetValue();
m_vr_gamma_value->setText(val <= 1.01f ? tr("Off") : QString::asprintf("%.1f", val));
};
update_gamma_label();
connect(m_vr_gamma, &ConfigFloatSlider::valueChanged, this, update_gamma_label);
// Auto Layer Spread
m_auto_layer_spread =
new ConfigBool(tr("Auto Layer Spread"), Config::GFX_VR_AUTO_LAYER_SPREAD);
openxr_layout->addWidget(m_auto_layer_spread, 13, 0, 1, 3);
m_layer_offset = new ConfigFloatSlider(Config::GFX_VR_LAYER_OFFSET_MIN,
Config::GFX_VR_LAYER_OFFSET_MAX,
Config::GFX_VR_LAYER_OFFSET,
Config::GFX_VR_LAYER_OFFSET_STEP);
m_layer_offset_value = new QLabel();
openxr_layout->addWidget(new ConfigFloatLabel(tr("Layer Offset:"), m_layer_offset), 14, 0);
openxr_layout->addWidget(m_layer_offset, 14, 1);
openxr_layout->addWidget(m_layer_offset_value, 14, 2);
m_layer_offset_value->setText(QString::asprintf("%.4f", m_layer_offset->GetValue()));
connect(m_layer_offset, &ConfigFloatSlider::valueChanged, this, [this] {
m_layer_offset_value->setText(QString::asprintf("%.4f", m_layer_offset->GetValue()));
});
m_element_depth = new ConfigFloatSlider(Config::GFX_VR_ELEMENT_DEPTH_MIN,
Config::GFX_VR_ELEMENT_DEPTH_MAX,
Config::GFX_VR_ELEMENT_DEPTH,
Config::GFX_VR_ELEMENT_DEPTH_STEP);
m_element_depth_value = new QLabel();
openxr_layout->addWidget(new ConfigFloatLabel(tr("Element Depth:"), m_element_depth), 15, 0);
openxr_layout->addWidget(m_element_depth, 15, 1);
openxr_layout->addWidget(m_element_depth_value, 15, 2);
m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue()));
connect(m_element_depth, &ConfigFloatSlider::valueChanged, this, [this] {
m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue()));
});
general_layout->addWidget(openxr_group);
auto* hacks_group = new QGroupBox(tr("VR Hacks"));
auto* hacks_group_layout = new QVBoxLayout;
hacks_group->setLayout(hacks_group_layout);
m_dont_clear_screen =
new ConfigBool(tr("Don't Clear Screen"), Config::GFX_VR_DONT_CLEAR_SCREEN);
m_disable_cpu_cull =
new ConfigBool(tr("Disable CPU Culling in VR"), Config::GFX_VR_DISABLE_CPU_CULL);
m_remove_bars = new ConfigBool(tr("Remove Cinematic Bars"), Config::GFX_VR_REMOVE_BARS);
m_lock_head_pose =
new ConfigBool(tr("Lock Head Pose Per Frame"), Config::GFX_VR_LOCK_HEAD_POSE);
hacks_group_layout->addWidget(m_auto_immediate_xfb);
hacks_group_layout->addWidget(m_lock_head_pose);
hacks_group_layout->addWidget(m_dont_clear_screen);
hacks_group_layout->addWidget(m_disable_cpu_cull);
hacks_group_layout->addWidget(m_remove_bars);
hack_layout->addWidget(hacks_group);
// Lock Head Pose is incompatible with Immediately Present XFB — when enabled,
// force-disable both Auto-enable Immediate XFB (local) and the Graphics Hacks
// Immediately Present XFB config key, and grey them out.
const auto update_lock_head_pose = [this] {
const bool locked = m_lock_head_pose->isChecked();
m_auto_immediate_xfb->setDisabled(locked);
if (locked)
{
Config::SetBaseOrCurrent(Config::GFX_VR_AUTO_IMMEDIATE_XFB, false);
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, false);
}
};
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(m_lock_head_pose, &QCheckBox::checkStateChanged, this,
[update_lock_head_pose](Qt::CheckState) { update_lock_head_pose(); });
#else
connect(m_lock_head_pose, &QCheckBox::stateChanged, this,
[update_lock_head_pose](int) { update_lock_head_pose(); });
#endif
update_lock_head_pose(); // Apply initial state
auto* tools_group = new QGroupBox(tr("Tools"));
auto* tools_layout = new QGridLayout;
tools_group->setLayout(tools_layout);
auto* add_hide_object_code_btn = new QPushButton(tr("Add Hide Object Code"));
connect(add_hide_object_code_btn, &QPushButton::clicked, this, [this] {
const std::string game_id = SConfig::GetInstance().GetGameID();
if (game_id.empty() || game_id == "00000000")
{
QMessageBox::information(this, tr("No Game Running"),
tr("Start a game before adding Hide Object codes."));
return;
}
auto codes = HideObjectEngine::LoadFromINI(game_id);
HideObjectAddEditDialog dialog(this, nullptr, codes);
if (dialog.exec() == QDialog::Accepted)
{
codes.push_back(dialog.GetResult());
HideObjectEngine::SaveToINI(game_id, codes);
HideObjectEngine::Engine::GetInstance().ApplyCodes(codes);
}
else
{
// Restore the active code set if temporary brute-force values were applied.
HideObjectEngine::Engine::GetInstance().ApplyCodes(codes);
}
});
tools_layout->addWidget(add_hide_object_code_btn, 0, 0);
auto* shader_hunter_btn = new QPushButton(tr("Shader Hunter"));
connect(shader_hunter_btn, &QPushButton::clicked, this, [this] {
const std::string game_id = SConfig::GetInstance().GetGameID();
if (game_id.empty() || game_id == "00000000")
{
QMessageBox::information(this, tr("No Game Running"),
tr("Start a game before searching for shaders."));
return;
}
if (m_shader_hunter_widget)
{
m_shader_hunter_widget->show();
m_shader_hunter_widget->raise();
m_shader_hunter_widget->activateWindow();
return;
}
m_shader_hunter_widget = new ShaderHunterWidget(this);
m_shader_hunter_widget->setAttribute(Qt::WA_DeleteOnClose, true);
m_shader_hunter_widget->show();
connect(m_shader_hunter_widget, &QObject::destroyed, this,
[this] { m_shader_hunter_widget = nullptr; });
});
tools_layout->addWidget(shader_hunter_btn, 1, 0);
auto* culling_finder_btn = new QPushButton(tr("Culling Code Finder"));
connect(culling_finder_btn, &QPushButton::clicked, this, [this] {
const std::string game_id = SConfig::GetInstance().GetGameID();
if (game_id.empty() || game_id == "00000000")
{
QMessageBox::information(this, tr("No Game Running"),
tr("Start a game before searching for culling codes."));
return;
}
if (m_culling_finder_widget)
{
m_culling_finder_widget->show();
m_culling_finder_widget->raise();
m_culling_finder_widget->activateWindow();
return;
}
m_culling_finder_widget = new CullingCodeFinderWidget(this);
m_culling_finder_widget->setAttribute(Qt::WA_DeleteOnClose, true);
m_culling_finder_widget->show();
connect(m_culling_finder_widget, &QObject::destroyed, this,
[this] { m_culling_finder_widget = nullptr; });
});
tools_layout->addWidget(culling_finder_btn, 2, 0);
m_load_custom_shaders =
new ConfigBool(tr("Load Custom Shaders"), Config::GFX_VR_LOAD_CUSTOM_SHADERS);
tools_layout->addWidget(m_load_custom_shaders, 3, 0);
m_enable_openxr_config_scene = new ConfigBool(
tr("Enable OpenXR Config Scene For Controller Binding"),
Config::GFX_VR_ENABLE_OPENXR_CONFIG_SCENE);
tools_layout->addWidget(m_enable_openxr_config_scene, 4, 0);
m_ar_mode_debug =
new ConfigBool(tr("Fake AR Mode (Debug)"), Config::GFX_VR_AR_MODE_DEBUG);
tools_layout->addWidget(m_ar_mode_debug, 5, 0);
general_layout->addStretch();
hack_layout->addStretch();
tools_tab_layout->addWidget(tools_group);
tools_tab_layout->addStretch();
main_layout->addWidget(tabs);
setLayout(main_layout);
AddDescriptions();
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&VRPane::OnEmulationStateChanged);
OnEmulationStateChanged(Core::GetState(Core::System::GetInstance()));
}
void VRPane::AddDescriptions()
{
static constexpr char TR_ENABLE_OPENXR_DESCRIPTION[] = QT_TR_NOOP(
"Enables VR rendering through OpenXR."
"<br><br>When enabled, Dolphin uses OpenXR instead of Stereoscopic 3D output modes."
"<br><br>This setting cannot be changed while emulation is active."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static constexpr char TR_AUTO_IMMEDIATE_XFB_DESCRIPTION[] = QT_TR_NOOP(
"Forces Immediately Present XFB on while OpenXR is enabled."
"<br><br>This improves head-tracking responsiveness and reduces latency in VR by presenting "
"each XFB copy as soon as it is created."
"<br><br>Turn this off if a game flickers with Immediately Present XFB, then use "
"<b>Lock Head Pose Per Frame</b> instead to keep head-tracking coherent."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_UNITS_PER_METER_DESCRIPTION[] = QT_TR_NOOP(
"Sets how many game world units correspond to one real-world meter."
"<br><br>Higher values increase stereo scale and make the world appear smaller."
"<br><br>Lower values decrease stereo scale and make the world appear larger.");
static constexpr char TR_LEAN_BACK_ANGLE_DESCRIPTION[] = QT_TR_NOOP(
"Applies a constant pitch offset (in degrees) to the VR camera."
"<br><br>Use this to compensate games where the default viewpoint feels tilted."
"<br><br>Positive values lean the camera back, negative values lean it forward.");
static constexpr char TR_CAMERA_FORWARD_DESCRIPTION[] = QT_TR_NOOP(
"Applies a constant forward/backward offset (in meters) to the VR camera."
"<br><br>Positive values move the camera forward, negative values move it backward.");
static constexpr char TR_VIRTUAL_SCREEN_DESCRIPTION[] = QT_TR_NOOP(
"Places 2D content (menus, FMV, in-game HUD) on a virtual screen floating in 3D space."
"<br><br>Disable for games that rely on full-screen EFB effects that don't work with a virtual screen.");
static constexpr char TR_SCREEN_DISTANCE_DESCRIPTION[] = QT_TR_NOOP(
"Distance in meters to the virtual screen used for 2D content (menus, FMV, HUD)."
"<br><br>The screen is fixed in space like a TV — it stays in place when you turn your head.");
static constexpr char TR_SCREEN_SIZE_DESCRIPTION[] = QT_TR_NOOP(
"Height in meters of the virtual screen used for 2D content."
"<br><br>Larger values make the screen bigger, smaller values make it more compact.");
static constexpr char TR_HEAD_LOCKED_CURVATURE_DESCRIPTION[] = QT_TR_NOOP(
"Curves the virtual screen used by Head Locked shader overrides."
"<br><br>0 keeps the screen flat. Higher values increase curvature."
"<br><br>This only affects shaders using Head Locked handling.");
static constexpr char TR_DONT_CLEAR_SCREEN_DESCRIPTION[] = QT_TR_NOOP(
"Prevents clearing the VR eye framebuffers before rendering each frame."
"<br><br>By default the screen is cleared to black, which prevents image trails when "
"looking outside the rendered area. Enable this if clearing causes visual glitches "
"in specific games."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static constexpr char TR_DISABLE_CPU_CULL_DESCRIPTION[] = QT_TR_NOOP(
"Disables CPU-side primitive culling when OpenXR VR is active."
"<br><br>This may fix missing geometry in some games at the cost of a small performance hit."
"<br><br>This only affects Dolphin's CPU culling optimization. It does not override "
"the game's own backface culling state.");
static constexpr char TR_OPCODE_REPLAY_DESCRIPTION[] = QT_TR_NOOP(
"Submits extra synthetic OpenXR frames between real emulation frames to better match a "
"90 Hz headset cadence."
"<br><br>60 to 90 replays one extra HMD-only frame every other real frame."
"<br><br>30 to 90 replays two extra HMD-only frames after each real frame."
"<br><br>This only affects OpenXR HMD output. The desktop mirror stays at the real game "
"frame cadence."
"<br><br><dolphin_emphasis>If unsure, leave this set to Off.</dolphin_emphasis>");
static constexpr char TR_AUTO_VBI_FROM_HMD_DESCRIPTION[] = QT_TR_NOOP(
"Forces Dolphin's VBI frequency to 90 Hz while OpenXR VR is enabled."
"<br><br>This overrides the Advanced tab's VBI percentage during VR sessions."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static constexpr char TR_AUTO_LAYER_SPREAD_DESCRIPTION[] = QT_TR_NOOP(
"Automatically spreads 2D elements (menus, HUD, text) on the virtual screen into separate "
"depth layers to prevent Z-fighting."
"<br><br>Each ortho draw call gets a unique layer index, with later draws closer to the "
"camera. Disable this if you prefer manual layer control via Shader Overrides."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_LAYER_OFFSET_DESCRIPTION[] = QT_TR_NOOP(
"Controls the depth separation between consecutive 2D layers on the virtual screen."
"<br><br>Higher values spread layers further apart (stronger Z-fighting fix but may look "
"wrong at extreme values). Lower values keep layers closer together."
"<br><br>Default: 0.0020");
static constexpr char TR_ELEMENT_DEPTH_DESCRIPTION[] = QT_TR_NOOP(
"Controls the depth range within individual 2D elements on the virtual screen."
"<br><br>Higher values give more depth separation within each element, fixing Z-fighting "
"that appears as flickering inside UI elements. Set to 0 to flatten elements completely."
"<br><br>Default: 0.0010");
static constexpr char TR_CLEAR_EFB_COPIES_DESCRIPTION[] = QT_TR_NOOP(
"Clears EFB (Embedded Framebuffer) copy textures to transparent when the copy's native "
"width is at or above this threshold. Set to 0 to disable."
"<br><br>Full-screen post-processing effects (bloom, blur) typically use full EFB width "
"(640). HUD and small UI elements use smaller partial copies. Increasing this value "
"preserves smaller EFB copies (like HUD textures) while removing large full-screen effects."
"<br><br>Recommended: 400-640 to clear only full-screen effects, or 0 to disable."
"<br><br><dolphin_emphasis>If unsure, leave this at 0 (off).</dolphin_emphasis>");
static constexpr char TR_REMOVE_BARS_DESCRIPTION[] = QT_TR_NOOP(
"Expands the scissor and viewport to fill the full active rendering area for perspective "
"draws, removing cinematic letterbox bars."
"<br><br>Some games (e.g. Metroid Prime) use GX scissor clipping to create cinematic "
"bars that mask the top and bottom of the screen. In VR these bars are distracting. "
"This option removes them by expanding the rendered area."
"<br><br>Disable this if it causes visual artifacts in a specific game."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_LOCK_HEAD_POSE_DESCRIPTION[] = QT_TR_NOOP(
"Snaps OpenXR head-tracking updates to game-frame boundaries (XFB copies) so every draw "
"call within a single game frame uses one consistent head pose."
"<br><br>Without this, when <b>Immediately Present XFB</b> is disabled, "
"<code>LocateViews()</code> can mutate the head pose mid-frame on the video thread, "
"causing different parts of the same frame (e.g. level geometry vs. dynamic objects) "
"to be rendered with different poses. The visible symptom is objects appearing to "
"&quot;stay in place&quot; and jump to their correct positions a few frames later."
"<br><br>This option captures a single head pose at the XFB-copy point in FIFO order, "
"guaranteeing all draws of a frame are coherent. It has no effect when "
"<b>Immediately Present XFB</b> is on (that path is already coherent)."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_VR_GAMMA_DESCRIPTION[] = QT_TR_NOOP(
"Adjusts gamma correction for VR eye output."
"<br><br>1.0 = no correction (default). 2.2 = standard sRGB gamma. "
"Higher values darken the image further."
"<br><br>Some headsets expect sRGB-encoded content and appear "
"too bright without correction. Adjust this slider until the brightness matches "
"the desktop mirror."
"<br><br><dolphin_emphasis>If unsure, leave this at 1.0 (Off).</dolphin_emphasis>");
static constexpr char TR_LOAD_CUSTOM_SHADERS_DESCRIPTION[] = QT_TR_NOOP(
"Loads custom shader replacements from the Load/Shaders/&lt;GameID&gt;/ directory."
"<br><br>Place edited shader files (dumped via Shader Hunter) in this folder to override "
"the game's original shaders. Files must be named &lt;hash&gt;-&lt;vs|ps|gs&gt;.txt."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static constexpr char TR_ENABLE_OPENXR_CONFIG_SCENE_DESCRIPTION[] = QT_TR_NOOP(
"Starts a temporary OpenXR scene while configuring an <b>OpenXR Wii Remote</b> with no "
"game running."
"<br><br>This keeps OpenXR controller input active so the mapping window can detect button "
"presses and motion input outside emulation."
"<br><br>If the temporary session cannot start, Dolphin falls back to the desktop-only "
"mapping window."
"<br><br>Disable this if your runtime or headset has trouble with the temporary binding "
"scene."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
static constexpr char TR_AR_MODE_DEBUG_DESCRIPTION[] = QT_TR_NOOP(
"Debug aid for AR Mode development on headsets that do <b>not</b> support passthrough."
"<br><br>Clears the per-eye OpenXR swapchain to <b>solid magenta</b> instead of "
"transparent black, so the regions that <i>would</i> be see-through in real AR Mode "
"become clearly visible. The OpenXR composition state is left as <code>OPAQUE</code>, "
"so this works on any headset and never triggers "
"<code>XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED</code>."
"<br><br>Use this to verify that the eye framebuffer clear path, layer flag wiring, "
"and game-geometry-on-empty-background composition all behave correctly without "
"needing a passthrough-capable runtime."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
static constexpr char TR_AR_MODE_DESCRIPTION[] = QT_TR_NOOP(
"Submits the OpenXR projection layer with alpha-blended composition so that empty "
"(transparent) pixels reveal the headset passthrough feed, making game geometry float "
"over the real world."
"<br><br>Requires a headset and runtime that support "
"<code>XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND</code> (e.g. Quest 3, Vive XR Elite, "
"Varjo, HoloLens). On opaque-only PCVR headsets (Index, Vive Pro) enabling this will "
"cause <code>xrEndFrame</code> to fail; disable it again if the VR session stops "
"submitting frames."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
m_enable_openxr->SetDescription(tr(TR_ENABLE_OPENXR_DESCRIPTION));
m_auto_immediate_xfb->SetDescription(tr(TR_AUTO_IMMEDIATE_XFB_DESCRIPTION));
m_units_per_meter->SetDescription(tr(TR_UNITS_PER_METER_DESCRIPTION));
m_lean_back_angle->SetDescription(tr(TR_LEAN_BACK_ANGLE_DESCRIPTION));
m_camera_forward->SetDescription(tr(TR_CAMERA_FORWARD_DESCRIPTION));
m_virtual_screen->SetDescription(tr(TR_VIRTUAL_SCREEN_DESCRIPTION));
m_screen_distance->SetDescription(tr(TR_SCREEN_DISTANCE_DESCRIPTION));
m_screen_size->SetDescription(tr(TR_SCREEN_SIZE_DESCRIPTION));
m_head_locked_curvature->SetDescription(tr(TR_HEAD_LOCKED_CURVATURE_DESCRIPTION));
m_dont_clear_screen->SetDescription(tr(TR_DONT_CLEAR_SCREEN_DESCRIPTION));
m_disable_cpu_cull->SetDescription(tr(TR_DISABLE_CPU_CULL_DESCRIPTION));
m_opcode_replay_mode->SetDescription(tr(TR_OPCODE_REPLAY_DESCRIPTION));
m_auto_vbi_from_hmd->SetDescription(tr(TR_AUTO_VBI_FROM_HMD_DESCRIPTION));
m_clear_efb_slider->SetDescription(tr(TR_CLEAR_EFB_COPIES_DESCRIPTION));
m_remove_bars->SetDescription(tr(TR_REMOVE_BARS_DESCRIPTION));
m_lock_head_pose->SetDescription(tr(TR_LOCK_HEAD_POSE_DESCRIPTION));
m_vr_gamma->SetDescription(tr(TR_VR_GAMMA_DESCRIPTION));
m_auto_layer_spread->SetDescription(tr(TR_AUTO_LAYER_SPREAD_DESCRIPTION));
m_layer_offset->SetDescription(tr(TR_LAYER_OFFSET_DESCRIPTION));
m_element_depth->SetDescription(tr(TR_ELEMENT_DEPTH_DESCRIPTION));
m_load_custom_shaders->SetDescription(tr(TR_LOAD_CUSTOM_SHADERS_DESCRIPTION));
m_enable_openxr_config_scene->SetDescription(tr(TR_ENABLE_OPENXR_CONFIG_SCENE_DESCRIPTION));
m_ar_mode->SetDescription(tr(TR_AR_MODE_DESCRIPTION));
m_ar_mode_debug->SetDescription(tr(TR_AR_MODE_DEBUG_DESCRIPTION));
}
void VRPane::OnEmulationStateChanged(Core::State state)
{
const bool running = state != Core::State::Uninitialized;
m_enable_openxr->setEnabled(!running);
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QPointer>
#include <QWidget>
class ConfigBool;
class ConfigFloatSlider;
class ConfigSlider;
class QLabel;
class CullingCodeFinderWidget;
class ShaderHunterWidget;
template <typename T>
class ConfigChoiceMap;
enum class OpenXROpcodeReplayMode : int;
namespace Core
{
enum class State;
}
class VRPane final : public QWidget
{
Q_OBJECT
public:
explicit VRPane(QWidget* parent = nullptr);
private:
void AddDescriptions();
void OnEmulationStateChanged(Core::State state);
ConfigBool* m_enable_openxr = nullptr;
ConfigBool* m_auto_immediate_xfb = nullptr;
ConfigFloatSlider* m_units_per_meter = nullptr;
QLabel* m_units_per_meter_value = nullptr;
ConfigFloatSlider* m_lean_back_angle = nullptr;
QLabel* m_lean_back_angle_value = nullptr;
ConfigFloatSlider* m_camera_forward = nullptr;
QLabel* m_camera_forward_value = nullptr;
ConfigBool* m_virtual_screen = nullptr;
ConfigFloatSlider* m_screen_distance = nullptr;
QLabel* m_screen_distance_value = nullptr;
ConfigFloatSlider* m_screen_size = nullptr;
QLabel* m_screen_size_value = nullptr;
ConfigFloatSlider* m_head_locked_curvature = nullptr;
QLabel* m_head_locked_curvature_value = nullptr;
ConfigBool* m_dont_clear_screen = nullptr;
ConfigBool* m_disable_cpu_cull = nullptr;
ConfigChoiceMap<OpenXROpcodeReplayMode>* m_opcode_replay_mode = nullptr;
ConfigBool* m_auto_vbi_from_hmd = nullptr;
ConfigSlider* m_clear_efb_slider = nullptr;
QLabel* m_clear_efb_value = nullptr;
ConfigBool* m_remove_bars = nullptr;
ConfigBool* m_lock_head_pose = nullptr;
ConfigBool* m_ar_mode = nullptr;
ConfigFloatSlider* m_ar_background_alpha = nullptr;
QLabel* m_ar_background_alpha_value = nullptr;
ConfigFloatSlider* m_vr_gamma = nullptr;
QLabel* m_vr_gamma_value = nullptr;
ConfigBool* m_auto_layer_spread = nullptr;
ConfigFloatSlider* m_layer_offset = nullptr;
QLabel* m_layer_offset_value = nullptr;
ConfigFloatSlider* m_element_depth = nullptr;
QLabel* m_element_depth_value = nullptr;
ConfigBool* m_load_custom_shaders = nullptr;
ConfigBool* m_enable_openxr_config_scene = nullptr;
ConfigBool* m_ar_mode_debug = nullptr;
QPointer<CullingCodeFinderWidget> m_culling_finder_widget;
QPointer<ShaderHunterWidget> m_shader_hunter_widget;
};
+6 -1
View File
@@ -128,13 +128,14 @@ void ToolBar::MakeActions()
m_config_action = addAction(tr("Config"), this, &ToolBar::SettingsPressed);
m_graphics_action = addAction(tr("Graphics"), this, &ToolBar::GraphicsPressed);
m_vr_action = addAction(tr("VR"), this, &ToolBar::VRPressed);
m_controllers_action = addAction(tr("Controllers"), this, &ToolBar::ControllersPressed);
// Ensure every button has about the same width
std::vector<QWidget*> items;
for (const auto& action :
{m_open_action, m_pause_play_action, m_stop_action, m_stop_action, m_fullscreen_action,
m_screenshot_action, m_config_action, m_graphics_action, m_controllers_action,
m_screenshot_action, m_config_action, m_graphics_action, m_vr_action, m_controllers_action,
m_step_action, m_step_over_action, m_step_out_action, m_skip_action, m_show_pc_action,
m_set_pc_action})
{
@@ -193,4 +194,8 @@ void ToolBar::UpdateIcons()
m_config_action->setIcon(Resources::GetThemeIcon("config"));
m_controllers_action->setIcon(Resources::GetThemeIcon("classic"));
m_graphics_action->setIcon(Resources::GetThemeIcon("graphics"));
QIcon vr_icon = Resources::GetThemeIcon("vr");
if (vr_icon.isNull())
vr_icon = Resources::GetThemeIcon("graphics");
m_vr_action->setIcon(vr_icon);
}
+2
View File
@@ -32,6 +32,7 @@ signals:
void SettingsPressed();
void ControllersPressed();
void GraphicsPressed();
void VRPressed();
void StepPressed();
void StepOverPressed();
@@ -57,6 +58,7 @@ private:
QAction* m_config_action;
QAction* m_controllers_action;
QAction* m_graphics_action;
QAction* m_vr_action;
QAction* m_step_action;
QAction* m_step_over_action;
+6
View File
@@ -108,6 +108,12 @@ if(WIN32)
ControllerInterface/ForceFeedback/ForceFeedbackDevice.cpp
ControllerInterface/ForceFeedback/ForceFeedbackDevice.h
)
if(ENABLE_VR)
target_sources(inputcommon PRIVATE
ControllerInterface/OpenXR/OpenXR.cpp
ControllerInterface/OpenXR/OpenXR.h
)
endif()
elseif(APPLE)
target_sources(inputcommon PRIVATE
ControllerInterface/Quartz/Quartz.h
@@ -55,9 +55,18 @@ Cursor::Cursor(std::string name_, std::string ui_name_)
// i18n: The symbol/abbreviation for degrees (unit of angular measure).
_trans("°"),
// i18n: Refers to emulated wii remote movements.
_trans("Total rotation about the pitch axis.")},
_trans("Total rotation about the pitch axis.")},
20, 0, 360);
AddSetting(&m_sensitivity_setting,
{_trans("Pointer Sensitivity"),
// i18n: The percent symbol.
_trans("%"),
_trans("Scales pointer movement. Lower values require larger controller movement for "
"the same cursor distance.")},
100, 10, 300);
AddSetting(&m_invert_x_setting, {_trans("Invert Left/Right")}, false);
AddSetting(&m_relative_setting, {_trans("Relative Input")}, false);
AddSetting(&m_autohide_setting, {_trans("Auto-Hide")}, false);
}
@@ -90,14 +99,33 @@ Cursor::StateData Cursor::GetState(const bool adjusted,
const ControllerEmu::InputOverrideFunction& override_func)
{
StateData state = GetState(adjusted);
const ControlState sensitivity_scale = m_sensitivity_setting.GetValue() / ControlState(100.0);
if (!override_func)
{
if (state.IsVisible())
{
state.x = std::clamp(state.x * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
state.y = std::clamp(state.y * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
if (m_invert_x_setting.GetValue())
state.x = -state.x;
}
return state;
}
if (const std::optional<ControlState> x_override = override_func(name, X_INPUT_OVERRIDE, state.x))
state.x = *x_override;
if (const std::optional<ControlState> y_override = override_func(name, Y_INPUT_OVERRIDE, state.y))
state.y = *y_override;
if (state.IsVisible())
{
state.x = std::clamp(state.x * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
state.y = std::clamp(state.y * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
if (m_invert_x_setting.GetValue())
state.x = -state.x;
}
return state;
}
@@ -66,6 +66,8 @@ private:
SettingValue<double> m_yaw_setting;
SettingValue<double> m_pitch_setting;
SettingValue<double> m_vertical_offset_setting;
SettingValue<double> m_sensitivity_setting;
SettingValue<bool> m_invert_x_setting;
SettingValue<bool> m_relative_setting;
SettingValue<bool> m_autohide_setting;
@@ -11,6 +11,9 @@
#ifdef CIFACE_USE_WIN32
#include "InputCommon/ControllerInterface/Win32/Win32.h"
#ifdef ENABLE_VR
#include "InputCommon/ControllerInterface/OpenXR/OpenXR.h"
#endif
#endif
#ifdef CIFACE_USE_XLIB
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
@@ -60,6 +63,9 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
#ifdef CIFACE_USE_WIN32
m_input_backends.emplace_back(ciface::Win32::CreateInputBackend(this));
#ifdef ENABLE_VR
m_input_backends.emplace_back(ciface::OpenXR::CreateInputBackend(this));
#endif
#endif
#ifdef CIFACE_USE_XLIB
m_input_backends.emplace_back(ciface::XInput2::CreateInputBackend(this));
+1
View File
@@ -276,6 +276,7 @@ void CreateDirectories()
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX));
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_JITBLOCKS_IDX));
File::CreateFullPath(File::GetUserPath(D_GAMESETTINGS_IDX));
File::CreateFullPath(File::GetUserPath(D_GAMESETTINGSVR_IDX));
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX));
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + EUR_DIR DIR_SEP);
@@ -34,3 +34,12 @@ if(MSVC)
# Add precompiled header
target_link_libraries(videod3d PRIVATE use_pch)
endif()
if(ENABLE_VR)
target_sources(videod3d PRIVATE
D3DOpenXR.cpp
D3DOpenXR.h
)
target_link_libraries(videod3d PRIVATE openxr_loader)
target_include_directories(videod3d PRIVATE "${CMAKE_SOURCE_DIR}/Externals/OpenXR/include")
endif()
+10
View File
@@ -122,6 +122,16 @@ void Gfx::SetPipeline(const AbstractPipeline* pipeline)
}
}
void Gfx::SetForcePixelShader(const AbstractShader* shader)
{
if (shader)
{
D3D::stateman->SetPixelShader(
static_cast<const DXShader*>(shader)->GetD3DPixelShader());
m_current_pipeline = nullptr; // force re-set on next SetPipeline
}
}
void Gfx::SetScissorRect(const MathUtil::Rectangle<int>& rc)
{
// TODO: Move to stateman
+1
View File
@@ -48,6 +48,7 @@ public:
std::vector<AbstractTexture*> additional_color_attachments) override;
void SetPipeline(const AbstractPipeline* pipeline) override;
void SetForcePixelShader(const AbstractShader* shader) override;
void SetFramebuffer(AbstractFramebuffer* framebuffer) override;
void SetAndDiscardFramebuffer(AbstractFramebuffer* framebuffer) override;
void SetAndClearFramebuffer(AbstractFramebuffer* framebuffer, const ClearColor& color_value = {},
+33
View File
@@ -19,6 +19,10 @@
#include "VideoBackends/D3D/D3DVertexManager.h"
#include "VideoBackends/D3DCommon/D3DCommon.h"
#ifdef ENABLE_VR
#include "VideoBackends/D3D/D3DOpenXR.h"
#endif
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/FramebufferManager.h"
#include "VideoCommon/ShaderCache.h"
@@ -26,6 +30,10 @@
#include "VideoCommon/VideoCommon.h"
#include "VideoCommon/VideoConfig.h"
#ifdef ENABLE_VR
#include "VideoCommon/VR/OpenXRManager.h"
#endif
namespace DX11
{
std::string VideoBackend::GetConfigName() const
@@ -147,6 +155,23 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
FillBackendInfo();
UpdateActiveConfig();
#ifdef ENABLE_VR
INFO_LOG_FMT(VIDEO, "VR: stereo_mode={} (OpenXR={})", static_cast<int>(g_ActiveConfig.stereo_mode),
g_ActiveConfig.stereo_mode == StereoMode::OpenXR ? "YES" : "NO");
if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR)
{
auto openxr = std::make_unique<DX11::D3DOpenXR>();
if (!openxr->Initialize())
{
WARN_LOG_FMT(VIDEO, "OpenXR initialization failed; continuing without VR.");
}
else
{
DX11::g_openxr_d3d = std::move(openxr);
}
}
#endif
std::unique_ptr<SwapChain> swap_chain;
if (wsi.render_surface && !(swap_chain = SwapChain::Create(wsi)))
{
@@ -167,6 +192,14 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
void VideoBackend::Shutdown()
{
#ifdef ENABLE_VR
// Shut down VR before the D3D device is destroyed.
// g_openxr_d3d first (releases DXTexture/DXFramebuffer wrappers),
// then g_openxr (calls xrDestroySwapchain/Session/Instance — releases runtime D3D11 refs).
// Both must happen before D3D::Destroy() to avoid "Unreleased references" warnings.
DX11::g_openxr_d3d.reset();
VR::g_openxr.reset();
#endif
ShutdownShared();
D3D::Destroy();
}
+329
View File
@@ -0,0 +1,329 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef ENABLE_VR
// Must define the D3D11 platform before any OpenXR includes.
#define XR_USE_GRAPHICS_API_D3D11
#include "VideoBackends/D3D/D3DOpenXR.h"
#include <algorithm>
#include <array>
#include <vector>
#include "Common/Assert.h"
#include "Common/Logging/Log.h"
#include "VideoBackends/D3D/D3DBase.h"
#include "VideoBackends/D3D/DXTexture.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/VR/OpenXRD3D11Common.h"
#include "VideoCommon/VR/OpenXRManager.h"
namespace DX11
{
std::unique_ptr<D3DOpenXR> g_openxr_d3d;
D3DOpenXR::D3DOpenXR() = default;
D3DOpenXR::~D3DOpenXR()
{
Shutdown();
}
bool D3DOpenXR::Initialize()
{
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Starting initialization...");
ASSERT_MSG(VIDEO, !VR::g_openxr, "OpenXRManager already initialized.");
auto mgr = std::make_unique<VR::OpenXRManager>();
// The D3D11 graphics binding extension is mandatory.
const std::vector<const char*> extensions = {XR_KHR_D3D11_ENABLE_EXTENSION_NAME};
if (!mgr->CreateInstance(extensions))
return false;
if (!mgr->InitializeSystem())
return false;
if (!mgr->EnumerateViewConfigurations())
return false;
// Publish the manager globally so CreateSessionD3D11 can access instance/system IDs.
VR::g_openxr = std::move(mgr);
if (!CreateSessionD3D11())
{
VR::g_openxr.reset();
return false;
}
if (!VR::g_openxr->CreateReferenceSpace())
{
VR::g_openxr.reset();
return false;
}
if (!CreateSwapchains())
{
VR::g_openxr.reset();
return false;
}
// Register this object as the swapchain provider so Presenter can acquire eye images.
VR::g_openxr->SetSwapchain(this);
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Initialization complete.");
return true;
}
void D3DOpenXR::Shutdown()
{
// Clear swapchain pointer before destroying swapchains so no dangling use occurs.
if (VR::g_openxr)
VR::g_openxr->SetSwapchain(nullptr);
DestroySwapchains();
VR::g_openxr.reset();
if (D3D::context)
D3D::context->Flush();
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Shut down.");
}
bool D3DOpenXR::CreateSessionD3D11()
{
ASSERT(D3D::device != nullptr);
ASSERT(VR::g_openxr != nullptr);
XrGraphicsRequirementsD3D11KHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR};
if (!VR::D3D11OpenXR::QueryGraphicsRequirements(VR::g_openxr->GetInstance(),
VR::g_openxr->GetSystemId(), &requirements))
return false;
INFO_LOG_FMT(VIDEO,
"OpenXR: D3D11 requirements — adapter LUID {:#010x}{:08x}, "
"min feature level {:#x}",
requirements.adapterLuid.HighPart, requirements.adapterLuid.LowPart,
static_cast<int>(requirements.minFeatureLevel));
XrSession session = XR_NULL_HANDLE;
if (!VR::D3D11OpenXR::CreateSessionFromRequirements(VR::g_openxr->GetInstance(),
VR::g_openxr->GetSystemId(), requirements,
D3D::device.Get(), &session))
return false;
VR::g_openxr->SetSession(session);
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Session created successfully.");
return true;
}
bool D3DOpenXR::CreateSwapchains()
{
ASSERT(VR::g_openxr != nullptr);
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
int64_t swapchain_format = 0;
if (!VR::D3D11OpenXR::SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format))
return false;
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& sc = m_eye_swapchains[eye];
sc.width = view_cfgs[eye].recommendedImageRectWidth;
sc.height = view_cfgs[eye].recommendedImageRectHeight;
// Format must come from xrEnumerateSwapchainFormats.
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
info.arraySize = 1;
info.format = swapchain_format;
info.width = sc.width;
info.height = sc.height;
info.mipCount = 1;
info.faceCount = 1;
info.sampleCount = 1;
// We render into this swapchain image as a color target, but never sample from it.
// Some runtimes reject the extra SAMPLED usage for certain formats.
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrCreateSwapchain failed for eye {} ({}).", eye,
static_cast<int>(result));
return false;
}
// Enumerate the D3D11 textures backing this swapchain.
uint32_t image_count = 0;
xrEnumerateSwapchainImages(sc.swapchain, 0, &image_count, nullptr);
std::vector<XrSwapchainImageD3D11KHR> images(image_count, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR});
xrEnumerateSwapchainImages(sc.swapchain, image_count, &image_count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data()));
sc.textures.resize(image_count);
sc.framebuffers.resize(image_count);
for (uint32_t i = 0; i < image_count; ++i)
{
// Adopt the runtime-owned texture. DXTexture::CreateAdopted reads the D3D11 texture
// descriptor to build the TextureConfig and AddRefs the underlying resource.
sc.textures[i] = DXTexture::CreateAdopted(ComPtr<ID3D11Texture2D>(images[i].texture));
if (!sc.textures[i])
{
ERROR_LOG_FMT(VIDEO, "OpenXR: DXTexture::CreateAdopted failed for eye {}, image {}.",
eye, i);
return false;
}
// No depth attachment for now; depth will be added in Phase 3 when the render path
// is fully integrated with the EFB.
sc.framebuffers[i] = DXFramebuffer::Create(sc.textures[i].get(), nullptr, {});
if (!sc.framebuffers[i])
{
ERROR_LOG_FMT(VIDEO, "OpenXR: DXFramebuffer::Create failed for eye {}, image {}.",
eye, i);
return false;
}
}
INFO_LOG_FMT(VIDEO, "OpenXR: Eye {} swapchain ready: {}x{}, {} images.", eye, sc.width,
sc.height, image_count);
}
return true;
}
void D3DOpenXR::DestroySwapchains()
{
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& sc = m_eye_swapchains[eye];
if (m_image_acquired[eye] && sc.swapchain != XR_NULL_HANDLE)
{
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const XrResult release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage during shutdown failed for eye {} ({}).",
eye, static_cast<int>(release_result));
}
m_image_acquired[eye] = false;
}
// Release Dolphin wrappers before destroying the swapchain so the
// runtime's textures are only freed after our references are gone.
sc.framebuffers.clear();
sc.textures.clear();
if (sc.swapchain != XR_NULL_HANDLE)
{
const XrResult destroy_result = xrDestroySwapchain(sc.swapchain);
if (XR_FAILED(destroy_result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrDestroySwapchain failed for eye {} ({}).", eye,
static_cast<int>(destroy_result));
}
sc.swapchain = XR_NULL_HANDLE;
}
}
}
AbstractFramebuffer* D3DOpenXR::AcquireEyeFramebuffer(uint32_t eye_index)
{
ASSERT(eye_index < 2);
auto& sc = m_eye_swapchains[eye_index];
XrSwapchainImageAcquireInfo acquire_info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
if (XR_FAILED(xrAcquireSwapchainImage(sc.swapchain, &acquire_info,
&m_acquired_image_index[eye_index])))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrAcquireSwapchainImage failed for eye {}.", eye_index);
return nullptr;
}
m_image_acquired[eye_index] = true;
// Block until the acquired image is safe to write.
XrSwapchainImageWaitInfo wait_info{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait_info.timeout = XR_INFINITE_DURATION;
if (XR_FAILED(xrWaitSwapchainImage(sc.swapchain, &wait_info)))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrWaitSwapchainImage failed for eye {}.", eye_index);
// Ensure we don't leak an acquired image if waiting fails.
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const XrResult release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
if (XR_FAILED(release_result))
{
WARN_LOG_FMT(VIDEO,
"OpenXR: xrReleaseSwapchainImage after wait failure failed for eye {} ({}).",
eye_index, static_cast<int>(release_result));
}
m_image_acquired[eye_index] = false;
return nullptr;
}
return sc.framebuffers[m_acquired_image_index[eye_index]].get();
}
void D3DOpenXR::ReleaseEyeTexture(uint32_t eye_index)
{
ASSERT(eye_index < 2);
if (!m_image_acquired[eye_index])
return;
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const XrResult result = xrReleaseSwapchainImage(m_eye_swapchains[eye_index].swapchain,
&release_info);
if (XR_FAILED(result))
{
WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye_index,
static_cast<int>(result));
}
m_image_acquired[eye_index] = false;
}
bool D3DOpenXR::SubmitFrame()
{
ASSERT(VR::g_openxr != nullptr);
const auto& eye_views = VR::g_openxr->GetEyeViews();
for (uint32_t eye = 0; eye < 2; ++eye)
{
auto& pv = m_projection_views[eye];
pv = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
pv.pose = eye_views[eye].pose;
pv.fov = eye_views[eye].fov;
pv.subImage.swapchain = m_eye_swapchains[eye].swapchain;
pv.subImage.imageArrayIndex = 0;
pv.subImage.imageRect = {
{0, 0},
{static_cast<int32_t>(m_eye_swapchains[eye].width),
static_cast<int32_t>(m_eye_swapchains[eye].height)}};
}
m_projection_layer = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
m_projection_layer.space = VR::g_openxr->GetReferenceSpace();
m_projection_layer.viewCount = 2;
m_projection_layer.views = m_projection_views.data();
if (VR::g_openxr->GetActiveBlendMode() == XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND)
{
m_projection_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
}
const std::vector<XrCompositionLayerBaseHeader*> layers = {
reinterpret_cast<XrCompositionLayerBaseHeader*>(&m_projection_layer)};
return VR::g_openxr->EndFrame(layers);
}
} // namespace DX11
#endif // ENABLE_VR
Loaded 100 of 3137 files, more files were not shown because too many files have changed in this diff. Show more